Obligement - L'Amiga au maximum

Vendredi 06 juin 2025 - 12:33  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in English


Réseaux sociaux

Suivez-nous sur X




Liste des jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z,
ALL


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Associations
Jeux
Logiciels
Matériel
Magazines et médias
Pages personnelles
Réparateurs
Revendeurs
Scène démo
Sites de téléchargement
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Programmation : Assembleur - réadaptation de Shadow Of The Beast
(Article écrit par Fabrice Labrador - janvier 2015)


1. Introduction
2. Environnement de développement
3. Rapide aperçu des capacités du matériel
4. Initialisation et restauration du système
5. Le fichier squelette pour notre réadaptation
6. Affichage du décor et défilement différentiel
7. Affichage du sprite de la bête
8. Déplacement de la bête à la manette
9. Affichage des ballons dirigeables au Blitter
10. Affichage des élements du décor au Blitter
11. Ajout de la musique et des bruitages
12. Conclusion


Chapitre 1 : Introduction [retour au plan]

Bonjour et bienvenue pour cet article sur la programmation du matériel de l'Amiga. Vous découvrirez ici comment exploiter et tirer parti des capacités graphiques et sonores de cette fabuleuse machine qu'est l'Amiga.

Pour cela, j'ai choisi de faire une mini-réadaptation du plus célèbre jeu sorti sur notre machine favorite, je veux bien sur parler de Shadow Of The Beast.

Nous verrons au cours des prochains chapitres comment reproduire, dans une version très basique, le premier niveau du jeu tout en gardant un niveau technique suffisamment simple pour que vous compreniez rapidement.

Avant tout, il faut que je vous parle des prérequis nécessaires au bon suivi de ce "cours", il vous faudra en premier une bonne connaissance de l'assembleur 68000. A ce propos, il existe d'excellent cours sur le net (par exemple celui d'Olivier Chatrenet : assembly68k.blogspot.fr). Il vous faudra aussi un Amiga (hé hé, ben oui bien sûr), alors qu'il soit réel ou émulé ne change rien, donc vous pouvez vous rabattre sur WinUAE pour commencer. Idéalement, il faudrait aussi un disque dur et un peu de mémoire, c'est un confort appréciable pour développer.

On peut maintenant passer à la configuration de votre environnement de développement.


Chapitre 2 : Environnement de développement [retour au plan]

Pour réaliser ce tutoriel, j'ai choisi d'utiliser l'assembleur Devpac 2 (disponible sur la disquette de couverture d'Amiga Format 39). Je l'utilise depuis des années et je suis vraiment plus à l'aise avec son interface qu'avec celle d'ASM Pro par exemple.

Utilisation de WinUAE

Créez-vous une configuration à base d'A500 (68000/OCS) en veillant bien à cocher la case "Cycle exact" dans l'onglet du "Chipset", ajoutez un peu de "Slow RAM" (512 ko voir 1 Mo) et ajoutez un disque dur de type "Directory", le mien s'appelle "System", et rendez-le amorçable. Copiez dessus le contenu de vos disquettes Workbench et Extras.

Je ne peux malheureusement pas vous mettre une archive de mon disque dur en téléchargement car le système Amiga est encore sous droits d'auteur et je risque de me faire lyncher si je fais ça. Si vraiment vous rencontrez des soucis à monter votre environnement, essayez de vous rabattre sur une compilation déjà toute faite du genre Amiga Forever.

Une fois votre disque dur en place, copiez le contenu de votre disquette Devpac dans un répertoire "Devpac2" sur votre disque dur.

Utilisation d'un Amiga

Si vous avez un disque dur, vous pouvez reprendre les étapes décrites ci-dessus, sinon le plus simple c'est de démarrer sur la disquette de Devpac 2 et d'avoir une autre disquette avec les sources dessus (j'ai longtemps développé comme ça sur Atari ST et sur Amiga).

Il ne vous reste plus qu'à récupérer l'archive des sources ici et à tout décompresser à la racine de votre disque dur, ou sur votre disquette de travail si vous n'avez pas de disque dur.

réadaptation de Shadow Of The Beast
Disque système et lancement de Devpac

réadaptation de Shadow Of The Beast
L'interface de Devpac 2


Chapitre 3 : Rapide aperçu des capacités du matériel [retour au plan]

Comme vous le savez, l'Amiga est une machine qui, grâce à ses capacités graphiques et sonores, fit sensation lors de sa sortie.

Contrairement à la majorité des machines de l'époque qui ne disposaient que d'un processeur central chargé de toutes les tâches, l'Amiga possède une ribambelle de puces spécialisées chargées des tâches courantes comme l'affichage vidéo, le son ou la copie de données, le 68000 faisant office de chef d'orchestre pour tout ce petit monde.

Et là où ça devient encore plus fort, c'est que toutes ces puces ont un accès direct à la mémoire, enfin seulement la mémoire Chip, sans déranger le 68000 dans son travail.

Les principales puces spécialisées que nous verrons par la suite sont le Copper (qui s'occupe principalement de la gestion de la vidéo), le Blitter (qui va nous permettre de copier des éléments graphiques) la puce sonore (alias Paula, qui nous permettra de jouer une musique) et la puce vidéo (pour afficher les plans de bits : jusqu'à 6 en Dual Playfield (double champ de jeu), ainsi que les sprites matériels, au nombre de 8 sur l'Amiga).


Chapitre 4 : Initialisation et restauration du système [retour au plan]

Avant de nous lancer dans le développement de notre petite réadaptation, il va nous falloir créer quelques fichiers qui contiendront des routines ou des constantes que nous réutiliserons fréquemment au cours de nos développements.

Nous regrouperons ces fichiers dans le sous-répertoire "Includes".

Pour commencer, nous allons récupérer un fichier qui définit les adresses de tous les registres des puces spécialisées de l'Amiga, il s'appelle "Register.s", ouvrez-le, vous voyez qu'il ne s'agit que d'une longue liste de noms suivis d'une adresse (ou d'un décalage). "CUSTOM" indique l'adresse de base des registres matériels de l'Amiga (à noter que les CIA ont leurs propres adresses de base), les autres noms correspondent aux registres des puces spécialisées. Pour ces derniers, on a seulement noté le décalage par rapport à l'adresse de base. Pour avoir le détail de la fonction de chaque registre, je vous conseille de vous rendre sur ce site : amiga-dev.wikidot.com/information:hardware#toc0.

Pour accéder par exemple à la première couleur de la palette, nous ferons un :

move.w #$f00,CUSTOM+COLOR00

Le fichier suivant s'appelle "Constant.s", il s'agit bien évidemment de constantes qui nous servirons dans nos développements, comme les décalages des fonctions de l'exec.library ou de la graphics.library. On trouve aussi des constantes pour gérer l'activation/désactivation des canaux DMA ou pour définir facilement le code opération du Blitter en fonction des sources que l'on veut utiliser.

Nous avons ensuite un fichier "Macro.s", il regroupe un ensemble de macros qui nous serviront à construire facilement notre liste Copper ainsi qu'une macro nous permettant d'attendre le faisceau vidéo à une position donnée.

Le dernier fichier qui nous intéresse s'appelle "System.s", ouvrez-le, il contient deux fonctions qui nous permettrons de sauvegarder et de restaurer tous les paramètres du système que nous allons modifier pour notre réadapatation. Ainsi, nous pourrons facilement revenir sous le Workbench en quittant notre programme sans devoir faire une réinitialisation et s'assurer également que le programme fonctionnera sur de nombreuses configurations.

La première fonction s'appelle judicieusement "SaveSystem", on commence par récupérer l'adresse de base de la bibliothèque Exec (ligne 17). Exec est la seule bibliothèque toujours ouverte et dont l'adresse de base est présente en mémoire, les autres bibliothèques doivent être ouvertes avant de pouvoir les utiliser.

On arrête ensuite la gestion du multitâche (ligne 20), ceci pour éviter qu'une autre tâche ne vienne nous prendre du temps processeur (une autre solution consisterait à passer la priorité de notre tâche au maximum).

On récupère ensuite la VBR (vector base register) (lignes 22 à 31), l'adresse de base de la table des vecteurs, on ne fait cela que si la machine possède au moins un processeur 68010. Pour récupérer cette adresse, il faut passer en mode superviseur, d'où l'utilisation de la fonction "Supervisor" de l'exec.library.

On ouvre ensuite la dos.library et la graphics.library (lignes 33 à 47), on regarde si on est en PAL ou en NTSC (lignes 49 à 61), ensuite on teste si le jeu de puces est AGA ou pas (lignes 63 et 68), puis on se réserve le Blitter (lignes 70 et 72), on sauvegarde la "view" et la liste Copper active et enfin on réinitialise la view afin de partir sur une base saine (lignes 74 à 80), le double "WaitTOF" permet de s'assurer que tout est bien Ok pour des écrans en entrelacé (donc avec deux listes Copper).

On sauve ensuite les vecteurs d'interruption du clavier et de la VBL (lignes 82 à 85) et pour finir on sauve le registre des interruptions et des canaux DMA (lignes 87 à 91) tout en positionnant l'attribut "SET" pour la restauration.

On indique que tout s'est bien passé et on sort de la fonction.

Voilà pour la sauvegarde, voyons la fonction "RestoreSystem" à présent. La première chose à faire est d'arrêter toutes les interruptions et les canaux DMA, puis de restaurer leurs valeurs initiales (lignes 117 à 122).

On restaure ensuite les vecteurs d'interruptions (lignes 124 à 127) ainsi que la liste Copper et la vue (lignes 129 à 135).

On libère le Blitter (lignes 137 et 139) et on peut fermer les bibliothèques Graphics et DOS (lignes 141 à 149).

Enfin, on rétablit le multitâche (ligne 152). Voilà, notre système est tout propre.

Les autres fichiers seront décrits dans les chapitres suivants.


Chapitre 5 : Le fichier squelette pour notre réadaptation [retour au plan]

Le fichier qui nous intéresse à présent pour cette première approche se nomme "Bones.s", il est à la racine du répertoire "Sources", c'est notre squelette pour tous les développements que nous ferons par la suite.

Chargez-le dans Devpac, assemblez-le (Amiga+A) et exécutez-le (Amiga+X) tel quel. Il se contente d'ouvrir un éran 2 couleurs et d'afficher des barres blanches verticales. Un clic sur le bouton gauche de la souris vous permet de quitter le programme.

réadaptation de Shadow Of The Beast
Chargement et compilation du fichier "Bones.s"

réadaptation de Shadow Of The Beast
Bones.s en action

Détaillons un peu son fonctionnement. Tout d'abord, nous allons inclure certains fichiers décrits au paragraphe précédant (lignes 7 à 14), puis il faut définir les canaux DMA à activer, dans notre cas, la vidéo et le Copper (ligne 17), puis les interruptions à activer, ici juste la VBL (ligne 20). Enfin, si on est en AGA, on peut spécifier le mode "burst" voulu (ligne 23), pour le coup on n'en veut pas.

On passe ensuite à la définition du "playfield" (notre structure écran, alias champ de jeu), largeur, hauteur, nombre de plans, si on est en plans de bits entrelacés ou pas et les modulos pour chaque ligne (lignes 28 à 34).

Petit intermède sur la façon dont l'Amiga gère son affichage : comme je le disais précédemment, l'Amiga peut afficher jusqu'à 6 plans de bits (8 en AGA), ce qui correspond à 64 couleurs (2 puissance 6), chaque plan de bits possède une adresse individuelle, rien ne vous oblige à les avoir à la suite en mémoire.

Il existe deux façons d'organiser ses plans de bits en mémoire :
  • Soit totalement indépendant les uns des autres. Dans ce cas, le modulo (le nombre d'octets à ajouter à l'adresse courante pour passer de la fin d'une ligne d'un plan de bits à la ligne suivante de ce même plan de bits) vaudra 0.
  • Soit entrelacés, première ligne du plan de bits 1, puis première ligne du plan de bits 2, etc. Dans ce cas, le modulo sera différent de 0, il correspondra en général à la largeur d'un plan de bits multiplié par le nombre de plan de bits moins 1 (ici (PF_WIDTH/8)*(PF_DEPTH-1)).
Cette dernière façon d'organiser les plans de bits rend plus simple la gestion des copies par le Blitter, une seule passe suffit à copier tous les plans de bits.

Si on a deux valeurs pour le modulo, c'est qu'en mode double champ de jeu on peut totalement dissocier les champs de jeu pairs des champs de jeu impairs. En passant, le modulo peut être négatif, ce qui permet des petits effets sympathiques comme un reflet vertical symétrique.

On démarre ensuite le programme à proprement parler, on sauve nos données système (lignes 41 à 43), on initialise notre tampon mémoire écran (ligne 46) et la liste Copper (ligne 47), on installe notre VBL (lignes 50 et 51), notre liste Copper (lignes 54 à 56), si on est en AGA, on paramètre le mode "burst" (lignes 58 à 60), puis on arrête toutes les interruptions et les canaux DMA avant de définir les nôtres (lignes 64 à 68).

On entre alors dans la boucle principale, on attend le passage de la VBL (lignes 73 à 76) et on regarde si on n'a pas appuyé sur le bouton gauche de la souris (ligne 78).

Si c'est le cas, on restaure le système et on quitte le programme (lignes 84 à 87) sinon on continue à boucler.

L'initialisation de l'écran est assez simple, on sauve l'adresse de notre tampon mémoire écran dans une variable (ligne 95) puis on dessine des lignes verticales dans l'écran (amusez-vous à changer la valeur du "move" pour avoir d'autres motifs), on copie le motif sur tout l'écran (lignes 95 à 100).

L'initialisation du Copper est un peu plus longue, il faut copier les adresses de nos plans de bits dans notre liste Copper (lignes 106 à 124), puis on initialise les adresses de nos sprites (lignes 126 à 135). Dans notre cas, on pointe simplement sur un sprite null (aucun affichage).

Le Copper a ceci de magique qu'il peut se synchroniser avec le balayage de l'écran, ce qui nous permet, par exemple, de changer la couleur du fond à partir d'une certaine ligne de l'écran, sachant qu'il peut accéder à quasi tous les registres des puces spécialisées de l'Amiga, on imagine de suite la puissance du truc.

Pour faire cela, il dispose de trois instructions. Oui, le Copper est un vrai coprocesseur qui peut exécuter un programme que l'on appelle "liste Copper" et qui est relancé à chaque début d'écran. La première instruction CWAIT permet d'attendre que le faisceau écran atteigne une position avant de passer à l'instruction suivante. La seconde instruction CMOVE permet de charger un registre matériel avec une valeur (au format mot). Enfin, la dernière instruction CSKIP permet de passer l'instruction suivante si le faisceau écran a atteint une certaine position (bon, celle là est quasi jamais utilisée).

Nous avons ensuite notre VBL, on se contente juste de "flagger" son passage et de libérer son vecteur (lignes 146 à 152). Par la suite, nous ferons plein de choses dans cette VBL.

Viennent ensuite les données du programme, le tampon mémoire pour notre écran, notre sprite null (qui n'affiche rien donc) et enfin notre liste Copper, détaillons-la.

On commence par définir la position de notre écran physique (lignes 181 et 182). En général, un écran basse résolution fait 320x256 mais rien n'empêche d'en faire un plus petit ou plus grand. A ce propos, l'écran de SOTB fait 288x200, ce qui permet de libérer des cycles DMA pour les sprites.

Les lignes suivantes initialisent les adresses de nos 8 sprites (lignes 184 à 199).

On attend ensuite quelques lignes puis on initialise notre écran. Tout d'abord, le "fetch start" et le "fetch stop" (lignes 202 et 203), je vais revenir là-dessus en fin de paragraphe. On définit ensuite le nombre de plan de bits à activer (ligne 204) ainsi que leur décalage, la priorité des plans de bits, les décalages de palette et les modulos (lignes 205 à 210). Je vous recommande d'aller voir sur le site Web que j'ai mentionné auparavant pour connaitre la signification de chacun de ces registres.

Viens ensuite l'initialisation des adresses de nos plans de bits (lignes 212 à 215) et enfin la définition des couleurs de la palette (lignes 217 et 218).

La dernière instruction se contente d'attendre une position qui n'existe pas évitant au Copper de continuer de parcourir la mémoire à la recherche de sa prochaine instruction. Le passage de la prochaine VBL réinitialisant son compteur de programme à la première instruction.

Finalement, on peut inclure nos fonctions système et autres en fin de programme (lignes 226 et +).

Fetching et composition d'une ligne écran

Le fetching sert à indiquer au contrôleur vidéo à partir de quelle position de ligne il doit commencer à décoder l'image en mémoire, pour pouvoir l'afficher, et à quelle position il peut arrêter son décodage.

Il faut savoir qu'une ligne vidéo est décomposée en cycles DMA qui représentent des fenêtres d'accès à la mémoire Chip pour les coprocesseurs et pour le 68000. Pour chaque ligne, un certain nombre de cycles est réservé pour le son, l'accès disque, les sprites, les plans de bits, le Blitter et bien sûr le 68000.

Ainsi, plus on a de plans de bits à afficher, moins on a de cycles disponibles pour le 68000 ou le Blitter (c'est pour cela que ça rame en haute résolution 16 couleurs). De plus, lorsque l'on veut avoir un écran plus grand ou que l'on souhaite faire un défilement horizontal, il faut rogner sur certains cycles DMA, ce sont les sprites qui sont alors sacrifiés. Dans un cas extrême, il ne reste plus qu'un seul sprite disponible pour l'affichage (c'est pourquoi Shadow Of The Beast a un écran plus petit, pour conserver tous les cycles DMA des sprites).

Aller, il est temps de passer à la pratique.


Chapitre 6 : Affichage du décor et défilement différentiel [retour au plan]

Les choses sérieuses commencent ici. Nous allons voir comment afficher et faire défiler le décor de fond de notre réadaptation, le fichier du décor se nomme "sotb_back.iff", jetez un oeil dessus pour voir à quoi il ressemble, c'est une image de 320x200 en 8 couleurs car nous allons utiliser le mode double champ de jeu qui nous offre deux écrans indépendants de trois plans chacun, l'autre partie du décor se trouve dans le fichier "sotb_sprite.iff".

La première étape consiste à décompresser nos images et à les copier dans notre tampon mémoire écran. Pour cela, nous allons appeler les fonctions du fichier "IFFTools.s" du répertoire "Includes". Je ne vais pas le décrire ici, vous verrez, il est assez commenté, sachez simplement qu'il permet de décoder un fichier ILBM en stockant d'un côté la palette de couleurs et de l'autre le corps de l'image en la décompressant si nécessaire.

Intéressons-nous plutôt au fichier "sotb_01.s" du répertoire "SOTB". Comme vous aller vous en rendre compte, nous retrouvons une bonne partie des choses décrites au chapitre précédent, ce qui est normal vue qu'il a comme base le fichier "Bones.s". On a rajouté un petit drapeau (flag) pour activer/désactiver le mode DEBUG, qui nous servira à voir le temps processeur occupé par notre programme.

On définit ensuite un certain nombre de constantes (lignes 33 à 113), d'abord le format de nos champs de jeu, 640x256 en trois plans (je reviendrais sur le pourquoi du 640 plus tard), le format de nos images et puis la description complète de tous les éléments du décor, à savoir, 11 niveaux de défilements différentiels répartis entre cinq niveaux de nuages (CLOUDX), un niveau de montagne (MOUNTAIN) et cinq niveaux d'herbe (GRASSX). A cela, il faut ajouter le dégradé du ciel (SKYX), la lune (MOON) et la barrière en bas de l'écran (FENCE), ça fait du monde.

Le programme démarre de façon classique avec la sauvegarde du système, l'initialisation du tampon mémoire écran et de la liste Copper. Vient ensuite le décodage et l'affichage des éléments du décor et enfin la préparation des défilements.

Mais comment ça marche un défilement sur Amiga au fait ? Alors voilà, c'est assez simple, commençons par le défilement vertical, pour obtenir un simple effet de défilement vertical, il suffit de changer l'adresse de notre plan de bits à chaque VBL, en augmentant cette adresse du nombre d'octets par ligne nous aurons l'impression que notre écran est descendu d'une ligne, il faut bien sûr se réserver un écran suffisamment haut pour pouvoir défiler dedans, en général le double de la hauteur affichée (même si d'autres techniques existent).

Prenons un exemple concret : vous avez un écran classique 320x256 en 16 couleurs (4 plans), pour le faire défiler verticalement, il va nous falloir définir un champ de jeu d'une hauteur double de celle de l'écran, donc on aura un champ de jeu de 320x512 et dont seule une partie sera affichée à un instant T, imaginez une sorte de fenêtre qui se balade dans notre champ de jeu. A chaque VBL, nous allons additionner à l'adresse source de nos plans de bits le nombre d'octets composant une ligne, dans ce cas 40 octets (320/8), ainsi à chaque VBL nous déplaçons notre fenêtre d'une ligne vers le bas. Bien sûr, une fois arrivé tout en bas, il faudra remonter au début du champ de jeu sinon on va balayer toute la mémoire Chip et ce n'est pas vraiment ce que l'on veut.

Pour le défilement horizontal, c'est un petit peu plus compliqué, l'adresse d'un plan de bits devant être un multiple de 2, nous ne pouvons pas défiler horizontalement de façon aussi simple que verticalement. Si nous changions juste l'adresse du plan de bits, nous ne pourrions défiler que par pas de 16 pixels, c'est assez moyen. Pour remédier à cela, il existe un registre (BPLCON1) qui nous permet d'indiquer au système un pas de défilement horizontal entre 0 et 15. Ce registre indique au système de combien de pixels il doit décaler l'écran vers la droite avant de l'afficher (en fait, ce registre indique combien la puce vidéo doit attendre de cycles pour démarrer l'affichage de la ligne, donc plus il attend, plus la ligne sera décalée vers la droite).

Il faut donc ruser un peu pour avoir un défilement fluide vers la gauche. Déjà, il faut démarrer le décodage vidéo 16 pixels avant le démarrage classique, c'est ce que l'on fait à la ligne 567, on a remplacé l'ancienne valeur $0038 par $0030, la puce vidéo démarre donc son décodage plus tôt. Il suffit ensuite, un peu comme pour le défilement vertical, de positionner correctement l'adresse du plan de bits pour donner l'illusion d'un défilement horizontal. Là aussi il convient de définir un écran plus large que la zone affichée pour se balader dedans (d'où le VP1_WIDTH qui vaut 640).

On a ainsi créé une table (lignes 455 à 457) qui va nous permettre de facilement convertir un décalage entre 0 et 15 en données pour le registre BPLCON1 et en décalage à ajouter à l'adresse du plan de bits.

Reprenons à présent le cours de notre fichier. L'étape suivante consiste à décoder nos images et à les copier dans nos champs de jeu. Tout d'abord, l'image du fond (nuages, montagne et herbes), c'est le rôle de la fonction "InitBackground" (lignes 204 à 226). Une fois l'image décodée nous allons la copier dans notre champ de jeu en doublant chaque ligne horizontalement pour couvrir toute la largeur du champ de jeu (nous aurons donc deux images identiques côte à côte). On refait quasiment la même chose avec la seconde image dans la fonction "InitForeground" (lignes 231 à 287) sauf que nous n'allons copier que deux éléments, la barrière (avec là aussi un doublage sur chaque ligne) et la lune qui ne sera pas doublée car cet élément reste fixe. On termine la fonction en initialisant les adresses des plans de bits dans la liste Copper.

Attention, en mode double champ de jeu, le premier champ de jeu correspond aux plans pairs (0, 2 et 4) et le second champ de jeu correspond aux plans impairs (1, 3 et 5), ceci explique le "addq.w #6,d1" de la ligne 283.

Bon, nos images sont en place, nous allons maintenant initialiser notre défilement, c'est le rôle de la fonction "PrepareScrolling" (lignes 292 à 307). Son but est de créer une table de 320 entrées qui nous permettra de déterminer facilement la valeur à mettre dans le registre BPLCON1 et le décalage à ajouter à l'adresse du plan de bits à partir d'une coordonnée X comprise entre 0 et 319 (donc un décalage complet de l'écran horizontalement).

La suite du programme est classique, initialisation de la VBL, de la liste Copper, des interruptions et des canaux DMA, on passe ensuite dans la boucle principale qui attend simplement un clic sur le bouton gauche de la souris.

Toute la magie du défilement se passe dans la VBL (lignes 318 à 337), allons voir ça. Pour faire défiler tout le monde, nous allons d'abord définir des tables (lignes 459 à 507) qui nous indiquerons pour chaque niveau de défilement la position actuelle du niveau (son décalage), la vitesse de défilement du niveau, l'adresse de la liste Copper qui va mettre à jour les registres en question et l'adresse initiale du plan de bits pour ce niveau de défilement.

La fonction "ScrollBackground" (lignes 341 à 369) va parcourir cette table et mettre à jour les données de chaque niveau du défilement, elle commence par récupérer la position actuelle du niveau (ligne 345), puis lui ajoute sa vitesse de décalage (ligne 346), elle teste ensuite un éventuel débordement (si on a dépassé le bord droit) et revient au début de la ligne si tel est le cas. On sauve la nouvelle position pour la prochaine VBL (ligne 351), à partir de cette position, et en nous aidant de la table du défilement, nous allons récupérer le décalage et le décalage écran que nous allons injecter dans notre liste Copper.

La fonction "ScrollForeground" (lignes 388 à 412) reproduit le même schéma mais pour le niveau de la barrière uniquement.

Il faut maintenant que je vous explique le rôle de la fonction "AssembleOffset", en effet, les valeurs de décalage des plans pairs et impairs sont regroupées dans le même registre (BPLCON1). Or, nous avons traité ces données de façon indépendante jusqu'à présent, si nous laissions les données telles quelles, un problème apparaîtrait juste après la barrière. En effet, une partie du troisième niveau d'herbe serait mal décalée car sa valeur aurait été réinitialisé par la donnée de décalage de la barrière. De même, le bas de la barrière serait mal décalé car soumis à la modification faite par les deux derniers niveaux d'herbe (pour vous en convaincre, vous pouvez mettre en commentaire l'appel à la fonction et observer le résultat). Il faut donc réinjecter les bonnes valeurs de décalage dans la liste Copper pour ces niveaux en tenant compte des valeurs de décalage des deux champs de jeu.

Détaillons un peu la liste Copper qui est en grande partie responsable de l'affichage. On définit donc un écran double champ de jeu en deux fois trois plans (ligne 571), on donne ensuite la priorité au plan 1 (ligne 572), cela à pour effet d'afficher les nuages devant la lune, on met en place les modulos des deux champs de jeu (lignes 575 et 576), puis vient le réglage ("setting") de la valeur de décalage et de l'adresse du plan de bits pour le premier niveau de défilement (nuages du haut). S'enchaîne ensuite la mise en place de la palette pour les nuages ainsi que le dégradé du ciel et les autres niveaux de défilement des nuages, puis on passe sur le niveau de la montagne, les derniers dégradés du ciel, les trois premiers niveaux de l'herbe, la barrière, pour laquelle on change la priorité des champs de jeu, ce qui l'affiche au premier plan et les deux derniers niveaux de l'herbe, à chaque fois on met en place le décalage écran (BPLCON1) et les adresses des plans de bits du champ de jeu, c'est une liste Copper assez simple en fait.

Voilà, quand on assemble et qu'on lance le programme, on a un résultat assez sympa, on peut passer à la suite, l'affichage du sprite de la bête.

réadaptation de Shadow Of The Beast
Le défilement différentiel


Chapitre 7 : Affichage du sprite de la bête [retour au plan]

Ouvrez le fichier "sotb_02.s", vous remarquerez qu'une grande partie du code a été reprise du fichier précédent, je ne détaillerais donc que les nouveautés.

On commence par ajouter l'activation du DMA sprite (ligne 19) puis on ajoute quelques constantes pour définir la taille et la position du sprite de la bête (lignes 116 à 120), celui-ci fait 32x52 pixels en 7 couleurs, on va donc avoir besoin de quatre sprites matériels pour l'afficher car, je le rappelle, un sprite matériel peut faire au maximum 16 pixels de large en 3 couleurs, il nous faudra donc coupler deux paires de sprites pour avoir au final un sprite de 32 pixels de large en 7 couleurs. C'est la méthode "InitSprite" qui va s'occuper de mettre en place nos deux paires de sprites.

Je passe rapidement sur la suite du programme qui reste identique au précédent : initialisation des champs de jeu, des interruptions et des canaux DMA, puis la boucle principale qui attends juste l'appuie sur le bouton gauche de la souris.

Venons en directement à la fonction d'initialisation des sprites (ligne 300). Afin de gérer facilement nos sprites et l'animation de ce dernier, nous avons recours à un ensemble de tables qui vont d'abord nous indiquer où se trouve notre sprite (en gros ses coordonnées dans l'image) puis qui vont décrire la façon de les animer. Allons voir ça de suite, avant de retourner à l'explication de notre fonction.

La table de définition de notre banque de sprites (ligne 684) indique simplement les coordonnées des sprites dans notre image "sotb_sprite.iff". Le premier travail de notre fonction va être de transformer ces coordonnées en adresses mémoire (lignes 301 à 315). Nous avons aussi besoin d'un peu d'espace en mémoire Chip pour stocker les sprites à afficher (lignes 730 à 748). Pour rappel, la définition d'un sprite c'est deux mots de contrôles suivi de "N" mots de données (un pour chaque plan du sprite à répéter pour toute la hauteur du sprite) et enfin deux mots NULL indiquant la fin de notre sprite. Notre fonction va ensuite initialiser la liste Copper avec les adresses de nos sprites (lignes 316 à 339).

Enfin, dernière étape de l'initialisation, nous allons déterminer les mots de contrôle des sprites. Là, c'est un peu plus compliquer en raison du format légèrement tordu de ces derniers. Tout d'abord, nous définissons les informations pour notre première paire de sprites (0 et 1), on récupère la position X et Y ainsi que la hauteur du sprite et on appelle la fonction magique "CalculSpriteControl". Cette fonction (lignes 376 à 396) va calculer les mots de contrôle du sprite à partir de ses coordonnées et de sa hauteur, je l'ai largement commenté pour que vous puissiez la comprendre facilement. On sauve ensuite ces mots de contrôle dans nos sprites en n'oubliant pas de définir l'attribut d'attachement pour le sprite impair. On recommence la procédure pour la seconde paire de sprite qui s'affichera 16 pixels après la première paire. Et voilà, nos sprites sont initialisés, on peut passer à leur animation.

Avant toute chose, nous allons définir les étapes de notre animation, c'est le rôle de la table "BeastRightAnimTable" (ligne 669), son format est assez trivial, le premier mot indique le nombre de trames (images) de l'animation, nous avons ensuite pour chaque frame le nombre de VBL pendant lesquelles la frame sera affichée (si vous le diminuez l'animation sera plus rapide), puis le pointeur sur le sprite à afficher pendant cette frame. Il nous reste à définir quelques variables qui nous serviront à contrôler l'animation, un compteur du nombre de trames à jouer avant de revenir à la première frame (ligne 667), un pointeur sur la frame courante à afficher (ligne 664) et un compteur de VBL pour savoir si on doit passer à la frame suivante (ligne 661).

L'animation est gérée pendant la VBL, c'est la fonction "AnimateSprite" (ligne 553) qui s'en charge et c'est assez simple. Tout d'abord, on regarde s'il faut afficher une nouvelle frame (ligne 554), si tel est le cas, il faut aussi vérifier que l'on n'a pas atteint la fin de liste d'animation (ligne 556), il faudrait alors repartir sur la première frame (ligne 558), puis on réinitialise le compteur de trames et on récupère l'adresse de la frame à afficher (lignes 560 et 561). Il nous suffit ensuite de recopier l'image du sprite en cours dans nos tampon mémoire, c'est le rôle de la function "CopySprite". A noter que nous n'utilisons que trois plans pour le sprite alors que nous pourrions en avoir quatre, mais je n'ai pas trouvé de sprites de la bête en 16 couleurs, désolé.

réadaptation de Shadow Of The Beast
La bête fait son entrée

Et voilà, c'est aussi simple que ça, on va pouvoir pimenter la chose dans le chapitre suivant en contrôlant tout ça avec la manette.


Chapitre 8 : Déplacement de la bête à la manette [retour au plan]

Avant de commenter le nouveau code source "sotb_03.s", nous allons faire un petit tour vers un autre fichier du répertoire "Includes" qui se nomme "Input.s" et qui, comme vous devez vous en douter, va nous servir à tester l'état des manettes et de la souris. Je ne vais pas m'étendre sur la partie contrôle de la souris car nous ne l'utiliserons pas pour notre démo.

Nous avons donc deux fonctions "CheckJoystick0" et "CheckJoystick1" qui vont nous permettre de tester l'état des manettes connectées aux ports 0 et 1. Le fonctionnement est archi simple, on récupère la donnée de la manette (ligne 54), on ne garde que les bits Y1, Y0 et X1, X0 (ligne 55) puis à l'aide d'une table, on détermine la position de la manette (lignes 57 à 60), il reste ensuite à tester le premier bouton feu (ligne 61) et le second (ligne 66). Ce dernier bouton est un peu spécial car il n'a pas vraiment de statut on/off mais plutôt un potard qui se déclenche à partir d'une certaine intensité, c'est d'ailleurs grâce à cela que l'on peut avoir autant de boutons feu sur la manette de la CD32, ceci nous oblige cependant à réinitialiser le potard de suite après sa consultation (ligne 70) pour obtenir une valeur cohérente au prochain test.

Retournons à présent à notre fichier source "sotb_03.s". Le début est extrêmement proche de la version précédente, on a juste ajouté trois appels de fonctions pour initialiser l'affichage (lignes 140 à 142).

Le principe de fonctionnement est le suivant, si la manette est en position centrale, le décor ne bouge plus et on affiche un sprite spécial représentant une position fixe. Si l'on pousse la manette vers la droite, le décor défile vers la gauche et le sprite démarre une course vers la droite et inversement si l'on pousse la manette vers la gauche.

Il va donc falloir compléter notre banque de sprite pour ajouter les sprites de position fixe et ceux de course vers la gauche (lignes 794 à 809), il nous faut aussi une table pour définir la séquence d'animation de la course vers la gauche (lignes 765 à 778). Si l'on veut gérer un changement de direction, il faut également que l'on puisse garder le sens de déplacement précédent (lignes 684 et 685).

Voilà, nos données sont définies, il ne reste plus qu'à ajouter la gestion de la manette au code.

Encore une fois, tout va se passer dans la VBL. La première chose à faire, c'est de tester la position de la manette, c'est le rôle de la fonction "CheckDirection" (lignes 462 à 476) qui commence par sauvegarder l'ancienne direction puis appel notre fonction de test de la manette "CheckJoystick1", on se contente ensuite d'indiquer dans quel sens notre sprite doit courir (0 ne bouge pas, 1 cours à gauche et -1 cours à droite).

La suite est simple, si on ne bouge pas, on se contente alors juste d'afficher le sprite de position fixe, sinon il faut faire comme avant, donc faire défiler le décor et afficher l'animation de course du sprite.

Dans les fonctions de défilement du décor "ScrollBackground" et "ScrollForeground" on va donc tenir compte du sens de déplacement pour faire défiler vers la droite ou la gauche simplement en ajoutant ou en soustrayant le pas du défilement en fonction de la direction (lignes 485 à 499 et lignes 539 à 553).

La technique est similaire pour l'animation du sprite, tant que l'on ne change pas de direction on se contente de jouer l'animation en cours (lignes 602 à 604), sinon on ruse en faisant croire que l'on a atteint la fin du cycle d'animation (lignes 605 et 606). On passe alors dans la partie classique de retour au début du cycle d'animation sauf que cette fois-là, on tient compte du sens de déplacement pour charger soit l'animation vers la gauche (ligne 615) soit l'animation vers la droite (ligne 618).

La fonction "FixedSprite" est nouvelle et son rôle est d'afficher le sprite de la bête en position fixe soit vers la droite, soit vers la gauche en se contentant de passer le bon sprite à la fonction "CopySprite".

réadaptation de Shadow Of The Beast
Hop, on ne bouge plus !

réadaptation de Shadow Of The Beast
Petite course à gauche

Et voilà, il ne vous reste plus qu'à tester. La prochaine étape va consister à afficher les deux ballons dirigeables qui passent de temps en temps au niveau de la lune.


Chapitre 9 : Affichage des ballons dirigeables au Blitter [retour au plan]

Alors là, ça commence à devenir sérieux, nous allons nous attaquer à une autre puce spécialisée célèbre de l'Amiga, je veux parler du Blitter. En effet, les deux ballons dirigeables seront en fait des BLOB que nous déplaceront horizontalement au niveau de la lune (pour information, dans le Shadow Of The Beast original, les ballons sont en fait des sprites).

Allons voir de suite le fichier "sotb_04.s", vous pouvez voir d'entrée que nous avons ajouté le canal DMA du Blitter à la liste des canaux DMA à activer (ligne 19), on a également ajouté quelques constantes pour la position verticale des ballons qui sera fixe (lignes 123 et 124). On a enfin défini un décalage de décalage du champ de jeu principal (ligne 127), cela nous permettra d'avoir de l'espace à gauche et à droite de la zone d'affichage. Ainsi, nous n'aurons pas à nous préoccuper d'un éventuel détourage des BLOB lors de leur affichage lorsque ces derniers seront en partie hors de la zone visible.

Une des nouvelles choses que nous allons faire, c'est bien sûr d'initialiser nos BLOB, c'est le rôle de la fonction "InitBlob" (lignes 436 à 472), tout comme pour les sprites nous avons défini une banque de BLOB (lignes 974 à 981) qui nous indique la position et la taille du BLOB dans notre image ainsi que la position du masque du BLOB. La fonction va parcourir cette table pour déterminer l'adresse du BLOB ainsi que la valeur que nous aurons à mettre dans le registre BLTSIZE du Blitter pour la copie de ce BLOB. Enfin, nous calculons également l'adresse du masque de notre BLOB.

Mais au fait, comment ça marche un BLOB et puis ça veut dire quoi en passant ? Bonne question, merci de l'avoir posée. BLOB c'est le diminutif de BLitter OBject, c'est donc un objet manipulé par le Blitter. Pour rappel, le Blitter est capable, entre autres, de déplacer des blocs de mémoire d'une adresse vers une autre, tout en leur faisant subir un certain nombre de transformations (par exemple un décalage, mais aussi des combinaisons logiques entre les blocs).

Pour cela, il possède trois canaux source A, B et C et un canal destination D (là où on va écrire), et bien sûr, un certain nombre de registres pour piloter tout ça (Cf. amiga-dev.wikidot.com/information:hardware#toc0 pour le détail sur ces registres).

Si on se contente d'afficher notre image du ballon, nous allons vite nous rendre compte qu'il y a un léger souci. En effet, le Blitter ne sachant manipuler que des objets "rectangulaire", nous n'aurons pas l'effet de transparence attendu (comme pour les sprites), il va donc falloir ruser, et c'est là qu'intervient le masque du BLOB. Grâce à lui, nous allons en gros indiquer au Blitter quels sont les pixels qui doivent être transparents lors de la copie. Le code qui permet de définir la combinaison entre les différentes sources du Blitter s'appelle le MINTERM, nous avons défini les deux principaux MINTERM dans notre fichier "Constant.s", il s'agit de la copie brute "BLT_COPY" et la copie avec masque "BLT_CCUT".

Reprenons le déroulement de notre programme, cette fois-ci nous allons faire nos animations de ballons non plus dans l'interruption VBL mais dans le corps principal du programme, c'est la fonction "DrawBalloon" qui va s'en charger.

Pour gérer l'animation des ballons, nous avons défini une simple table "BlobSequence" qui pour chaque BLOB indique l'adresse du BLOB et de son masque, la position X courante du BLOB, son pas d'incrément, sa position de départ et sa position limite d'affichage avant de revenir à sa position initiale.

Petite remarque en passant, si l'on se contente d'utiliser un pas entier, on va avoir une vitesse de déplacement minimale de 1 pixel par VBL, ça peut paraître peu mais, en fait, c'est carrément trop rapide pour l'affichage de nos ballons qui ressembleraient plus à des jets qu'à des dirigeables. Il faut donc ruser, pour cela nous utiliserons un pas d'incrément non entier, par exemple 0,25 pour déplacer le BLOB de 0,25 pixel par VBL. Et là, on a un nouveau souci : en effet, le 68000 n'est pas très fort pour manipuler des nombres à virgule, on va donc utiliser le concept des nombres à virgules fixe, ça consiste simplement à prendre un entier et à réserver un certain nombre de bits pour simuler la partie décimale. Dans notre cas, sur un nombre de 16 bits, nous allons réserver 4 bits pour la partie décimale, ce qui nous donne une précision de 1/16e de pixels pour le pas de déplacement. Par contre, ça nous oblige à multiplier toutes les coordonnées entières par 16 pour avoir des calculs cohérents, cela explique pourquoi dans la table on trouve une position courante définie par 80x16 (qui correspond en fait à une coordonnée entière X = 80).

Donc, au final, nous avons le premier dirigeable, le grand, qui va se déplacer de 0,5 pixel à chaque VBL et le second dirigeable, le petit, qui se déplacera de -0,25 pixels par VBL (donc vers la gauche).

Revenons à notre fonction. On commence par charger notre table d'animation (ligne 500), on récupère la position courante du BLOB et on lui ajoute son pas d'incrément (lignes 502 et 503), on vérifie qu'il n'a pas atteint sa position limite, sinon on le fait revenir à sa position de départ. Et finalement, on sauve la nouvelle position (ligne 508).

Pour avoir une position en X entière, il nous faut supprimer la partie décimale qui tient sur 4 bits, c'est pourquoi nous faisons un petit décalage à droite de 4 bits (ligne 509), on met en place la position Y du BLOB dans d1 et on appelle la fonction "DrawBlob" qui va nous afficher tout ça à l'écran.

L'affichage d'un BLOB suit en principe toujours les mêmes étapes, on calcule l'adresse de destination ainsi que les modulos source et destination et enfin le décalage à faire subir à la source (car l'adresse ne nous permet que d'avoir une précision de 16 pixels). Une chose à savoir, c'est que le fait de demander au Blitter de faire un décalage de la source va nous obliger à lire deux octets de plus de nos données source (c'est un peu comme si on offrait un petit tampon mémoire de travail au Blitter pour faire son décalage), il faudra en tenir compte lorsque l'on va calculer les modulos.

Commençons par le calcul du décalage et de l'adresse de destination. On commence par isoler la coordonnée X du BLOB, on masque les quatre premiers bits afin d'avoir une coordonnée multiple de 16 (ligne 532), on divise tout ça par 8 (ligne 533) pour obtenir le nombre d'octets à ajouter pour notre position, puis on calcule l'adresse en Y en multipliant notre coordonnée Y par la taille en octet d'une ligne du champ de jeu (ligne 534). On additionne les deux valeurs et on ajoute enfin l'adresse de base de notre champ de jeu (ligne 539). Voilà, on a notre adresse de destination. Les adresses sources du BLOB et du masque ont déjà été calculées lors de la phase d'initialisation, on se contente donc de les récupérer (lignes 536 et 537).

Pour calculer les modulos, il suffit de connaître la largeur en octets de notre BLOB à laquelle on ajoute 2 pour permettre au Blitter de faire son décalage, on soustrait ensuite cette largeur à la largeur en octet d'une ligne de notre image source ainsi que de notre champ de jeu de destination (lignes 541 à 548). Enfin, on calcule le décalage à faire simplement en masquant les quatre premiers bits de la position en X (ligne 550).

Voilà, nous avons toutes nos données, il suffit maintenant de les fournir au Blitter pour qu'il affiche nos ballons, mais avant ça, il faut attendre la fin de l'opération précédante, c'est ce que fait la fonction "WaitBlit" (ligne 554), on pourrait interroger directement le registre d'activité du Blitter mais c'est plus sur de passer par cette fonction.

On commence par lui fournir les adresses source et destination. Alors, pour bien comprendre la suite, il faut savoir que le masque sera affecté à la source A, le BLOB sera affecté à la source B, la source C pointera sur la destination (ça peut sembler bizarre mais ça permet d'utiliser la destination dans les combinaisons logique que peut faire le Blitter), et bien sûr, la destination sera affectée (en plus) au canal D (lignes 556 à 559).

On renseigne les masques à appliquer aux données lues par le Blitter (ligne 560), dans notre cas on demande au Blitter de ne pas tenir compte du dernier mot lu (c'est son tampon mémoire de travail, il ne doit pas être copié). Puis, on lui fournit les valeurs des modulos pour l'ensemble des canaux (lignes 561 à 565), à la fois source et destination.

Mais au fait, pourquoi doit-on utiliser la destination comme source du Blitter ? Bonne question (ah mince, je l'ai déjà faite celle-là), en fait il faut revenir au mode de fonctionnement du Blitter, ce dernier commence par récupérer les données de chaque source active puis il leur fait subir d'abord le décalage (pour les sources A et B), puis l'opération logique sélectionnée et enfin il copie le résultat dans la destination. Donc, si on veut appliquer une opération logique sur la destination (comme masquer les pixels qui seront écrasés par notre BLOB), il faut qu'elle fasse partie également des données source.

Donc, dans notre cas, nous allons faire les opérations suivantes (on appelle cela "cookie cut") : d'abord, nous allons masquer les pixels transparents de notre BLOB à l'aide du masque (SOURCE A & SOURCE B), puis nous allons masquer les pixels de la destination qui seront écrasés par notre BLOB (!SOURCE A & SOURCE C). Enfin, nous allons combiner nos deux morceaux par un "ou" logique, ainsi les pixels non transparents de notre BLOB viendront prendre la place des pixels masqués de la destination.

On fournit donc le code opération au Blitter ainsi que le décalage à faire pour les sources A et B (lignes 565 à 567). Enfin, pour faire démarrer le Blitter, il suffit d'écrire dans le registre BLTSIZE (ligne 568) la taille des données à copier que nous avions déjà calculé lors de l'initialisation de la banque des BLOB.

Et voilà, nos ballons s'affichent. Par contre, si on se contente de ça, on va vite avoir un souci, déjà lorsque l'on va copier notre ballon à sa position suivante, il va rester un bout de l'ancienne position qui ne sera pas écrasée par le nouveau BLOB (en plus de tous les pixels transparents) et enfin, lorsque le ballon va arriver au niveau de la lune, il va écraser l'image et on ne pourra plus la voir. Mais alors que faire ? Et bien c'est simple, il faut restaurer la partie écrasée par le BLOB à chaque déplacement de ce dernier.

C'est exactement le rôle de la fonction "RestoreMoon", alors pour le coup, on va la jouer un peu bourrin car nous allons restaurer toute l'image de la lune et pas seulement la partie écrasée par le BLOB. L'opération se fait entièrement au 68000 à coup de "movem.l", on aurait encore pu le faire au Blitter mais il faut savoir varier les plaisirs. La fonction est appelée juste après que la vidéo ait affiché la zone de la lune. Pour ça, on attend la ligne de raster qui va bien (ligne 191) et on lance la restauration. On aurait pu faire l'opération avant la copie des ballons mais comme on est sûr de pouvoir tout afficher dans la frame, on peut se permettre de restaurer l'écran de suite après son affichage (attention, si on descend sous la frame, on va avoir un effet de scintillement avec cette méthode).

réadaptation de Shadow Of The Beast
Oh, le beau ballon bleu !

Bien, notre démo commence à avoir de l'allure, passons maintenant à une autre partie essentielle de l'animation, l'affichage des éléments du décor de premier plan.


Chapitre 10 : Affichage des éléments du décor au Blitter [retour au plan]

Bon alors, c'est bien sympa de courir partout mais c'est un peu vide comme décor pour le moment, il faudrait remplir un peu tout ça. Et ça tombe bien, c'est exactement ce que nous allons faire dans cette partie.

Pour commencer, vous pouvez jeter un coup d'oeil au fichier "sotb_tile.iff", vous y trouverez les quelques éléments graphiques qui constitueront notre décor.

Voici en gros comment on va procéder : on va imaginer que notre sprite se déplace dans un champ de jeu virtuel qui ferait 3200 pixels de large (soit 10 écrans), nous aurons donc une coordonnée virtuelle X pour le sprite qui pourra évoluer entre 0 et 3200.

Nous aurons des éléments graphiques qui seront disposés à des positions fixes dans notre champ de jeu virtuel, il suffira donc de les afficher lorsque notre sprite arrivera à leur proximité (en gros lorsqu'ils rentreront dans le champ de visibilité de l'écran).

Voyons cela de suite à partir du fichier "sotb_05.s". On commence comme d'habitude par la définition de nouvelles constantes, d'abord pour définir le format de l'image des éléments du décor (lignes 133 à 135) que nous appelerons TILE, puis pour définir l'aspect de notre champ de jeu virtuel (lignes 138 à 143) que nous appelerons MAP et enfin pour définir les positions de nos éléments de décor (lignes 146 à 155).

L'étape suivante consiste bien sûr à initialiser nos TILE, c'est le rôle des fonctions "InitMap" et "InitTile". La fonction "InitMap" est assez simple, elle va décompresser l'image, puis transférer nos éléments dans un tampon mémoire situé en mémoire Chip pour permettre au Blitter d'y accéder.

Pour décrire nos éléments, nous allons utiliser une méthode éprouvée, à savoir une table de définition qui va s'appeler "TileBank" (lignes 1193 à 1202). Au départ, cette table nous indique les coordonnées de chaque TILE dans notre image, la fonction "InitTile" va transformer ces coordonnées en adresse et va également calculer le mot de donnée à fournir au Blitter pour lancer la copie du TILE.

Comme je l'expliquais plus haut, nous allons faire évoluer notre sprite dans un champ de jeu virtuel de 3200 pixels. Pour gérer sa position à un instant T, nous allons simplement nous appuyer sur son sens de déplacement et faire varier sa coordonnée X en fonction, il faudra également penser à gérer le cas où l'on a atteint la limite gauche ou droite, tout cela se passe dans la VBL (lignes 786 à 801). On commence par tester le sens de déplacement et en fonction de cela, on incrémente ou on décrémente la position du sprite, on teste si l'on a pas dépassé les limites du champ de jeu, puis on sauve la nouvelle position, du très classique en fait.

Passons à l'affichage de nos TILE, nous ferons ça dans la boucle principale, juste après l'affichage des ballons, c'est le rôle de la fonction "DrawMap". Pour ce faire, nous allons parcourir une table qui décrit la position de chaque élément du décor, cette table s'appelle "TileMap", sa structure est relativement simple, on indique d'abord le TILE que l'on souhaite afficher puis ses coordonnées X et Y, du très basique en fait. Il suffit donc à chaque VBL de parcourir la table et de tester si l'élément en question est visible par rapport à la position de notre sprite.

On commence donc par récupérer la position courante dans le champ de jeu virtuel (ligne 689), puis pour chaque TILE son adresse, si elle est nulle, cela veut dire que l'on a atteint la fin de la liste et que l'on peut quitter la fonction. Sinon, on récupère les coordonnées X et Y du TILE, on va ensuite calculer si le TILE est visible à l'écran, pour cela rien de compliqué, il suffit de regarder si la coordonnée X du TILE est comprise dans notre écran visible ou si la coordonnée X + la largeur du TILE est comprise dans notre écran visible. Pour définir l'écran visible, il suffit de prendre la coordonnée X de la position courante du sprite "BeastPosition" qui constituera la borne inférieure et de lui ajouter la largeur de l'écran pour déterminer la borne supérieure (lignes 696 à 708). Si on détecte que le TILE est visible à l'écran alors on appelle la fonction "DrawTile".

Dans notre cas, l'affichage des TILE se fait par une copie simple au Blitter. En effet, les éléments du décors n'ont pas besoin d'utiliser un masque d'affichage car ils ne sont pas copiés au dessus d'une autre image mais directement sur le fond du champ de jeu. En dehors de ça, la méthode de définition des données à fournir au Blitter reste très proche de celle utilisée pour l'affichage des ballons. On va donc commencer par calculer l'adresse d'affichage du TILE, puis les modulos source et destination et enfin le pas de décalage du TILE, la réelle différence avec la fonction "DrawBlob" vient du fait que nous n'avons besoin que d'une source et d'une destination pour faire une copie brute, donc le TILE utilisera la source A et la destination la source D (lignes 744 à 751).

Et comme pour les ballons, il faut penser à nettoyer l'écran entre deux VBL avant de réafficher nos TILE. Là aussi, on va simplement attendre que la vidéo ait affiché notre décor pour déclencher le nettoyage (ligne 234). On appelle ensuite la fonction "ClearMap" qui va se charger de cette tâche.

Pour le coup, j'ai choisi d'utiliser le Blitter pour effacer la zone écran. Alors là, on a plusieurs solutions, la plus classique consiste à activer uniquement le canal D en fournissant l'adresse de la zone à effacer puis à démarrer le Blitter de façon standard. Mais je voulais vous montrer comment faire une copie brute sans décalage, donc du coup j'ai choisi une autre méthode, j'ai déclaré un petit tampon mémoire de la taille d'une ligne écran (ligne 1257) et je demande au Blitter de recopier cette ligne sur l'ensemble de la zone écran à effacer. La ruse consiste à fournir un modulo négatif pour la source (ligne 765). Ainsi, lorsque le Blitter a fini de copier la ligne, il recale son pointeur sur le début du tampon mémoire avant d'attaquer la ligne suivante.

Et voilà le résultat, en avant la balade.

réadaptation de Shadow Of The Beast
Bonjour monsieur !

Bien, il ne nous reste plus qu'à apporter la touche finale en ajoutant un peu de musique et du son à notre démo.


Chapitre 11 : Ajout de la musique et des bruitages [retour au plan]

Dernière étape de notre démo, l'ajout d'une musique et d'un bruitage assez simple. Ouvrez le fichier "sotb_06.s", on va regarder tout ça ensemble. Comme d'habitude, on va commencer par ajouter le DMA audio à la liste des canaux DMA à activer (ligne 19).

Avant de continuer, nous allons nous intéresser à la routine musicale, elle se trouve dans le fichier "ModPlayer.s". Je vous le dis de suite, nous n'allons pas la décrire en détail car, d'une part, c'est long et assez complexe et, d'autre part, ce n'est pas moi qui l'ai écrite. En effet, cette routine est l'oeuvre de Frank Wille qui l'a utilisé pour un jeu de plates-formes. Son principal intérêt, outre le fait qu'elle soit disponible sur Aminet, c'est que l'on peu contrôler facilement le volume général de la musique et qu'on peut lui faire jouer des bruitages en même temps qu'elle joue de la musique, elle va automatiquement bloquer un canal son (parmi les quatre disponibles) pour jouer le bruitage puis va reprendre la musique sur ce canal. Enfin, elle utilise le timer (chronomètre) CIA pour exécuter sa routine principale, ce qui lui permet de rejouer des modules à vitesse variable, ce que ne sait pas faire une routine qui s'exécute uniquement dans la VBL.

Très bien, vous avez remarqué que le début du programme ne change quasiment pas, on a juste ajouté un appel à la fonction "InitSound" dans la partie des initialisations. Cette fonction (lignes 613 à 636) a pour rôle de décoder le fichier sonore au format IFF 8SVX et à stocker l'échantillon sonore dans un tampon mémoire ainsi que ses paramètres dans des variables. Pour décoder le fichier, nous faisons appel à la routine "DecodeSound" qui se trouve dans le fichier "IFFTool.s", cette dernière ressemble à la routine qui nous sert à décoder les fichiers IFF ILBM, je pense que les commentaires suffiront à sa compréhension.

Sur Amiga, un son est caractérisé par quatre paramètres, son adresse (l'emplacement de l'échantillon), sa longueur (le nombre de mots qui composent l'échantillon), son volume (compris entre 0 et 64), et sa période (le temps que le système doit attendre avant de jouer le prochain octet de l'échantillon). En gros, plus la période est courte plus le son va paraître aigue et plus elle est longue, plus il va paraître grave. L'Amiga dispose de quatre canaux sonores en stéréo (deux à gauche et deux à droite) qui sont paramétrables de façon totalement indépendante. Chacun possède son propre volume, sa propre période et son propre canal DMA. A noter que, par défaut, un son envoyé sur un canal sera joué en boucle tant que l'on ne dit pas explicitement au système de l'arrêter (en coupant son canal DMA par exemple).

Revenons à notre programme. Nous avons défini un certain nombre de variables qui vont nous servir à contrôler nos sons, tout d'abord nous allons stocker le volume de la musique (ligne 1128), cela nous permettra de faire un effet de fondu entrant/fondu sortant sur le morceau à jouer. Nous stockons aussi les paramètres du bruitage (ligne 1131), on y retrouve sa longueur, son volume et sa période, on se réserve une petite variable pour indiquer si on a appuyé sur le bouton feu (ligne 1140). Il faut ensuite inclure notre son (ligne 1327), notre module (ligne 1360) qui sera en mémoire Chip pour que le DMA puisse y accéder, et enfin un tampon mémoire pour notre échantillon (ligne 1364) également en mémoire Chip. Ok, nous avons tout on va pouvoir faire du bruit.

Une fois l'initialisation du Copper et des interruptions faites, nous allons initialiser la musique (lignes 205 à 213). Pour cela, il suffit d'appeler les deux méthodes "mt_install_cia" qui va mettre en place le timer CIA, puis "mt_init" qui va initialiser le module à jouer, le démarrage de la musique se fait en positionnant la variable "mt_Enable" à "true". On peut ensuite lancer notre petite routine de fondu entrant (fade-in : monté progressive du volume) qui s'appelle "FadeMusicIn" (lignes 643 à 653). Pour cela, on se contente simplement d'augmenter le volume de 1 à chaque VBL jusqu'à atteindre le volume maximal de 64, le changement de volume se fait en appelant la fonction "mt_mastervol", cette fonction change le volume de tous les canaux sonore. On utilisera la même technique pour le fondu sortant en diminuant le volume de 1 à chaque VBL.

Nous allons maintenant jouer notre bruitage à chaque fois que l'on appuie sur le bouton feu de la manette. Pour cela, nous commençons par contrôler l'état du bouton feu à chaque VBL, on fait cela dans la fonction "CheckDirection" en vérifiant si le bit du bouton feu est à 1 (ligne 910), si tel est le cas on l'indique dans notre variable (ligne 912). Il suffit ensuite dans la VBL de vérifier l'état de notre variable puis de lancer le son en conséquence en fournissant ses paramètres à la fonction "mt_soundfx", carrément très simple, ensuite nous repassons notre drapeau "PressFire" à 0 pour éviter de relancer le son à la prochaine VBL. Et voilà, notre bête peut pousser son cri d'horreur.

La fin du programme change un petit peu, nous en profitons pour faire un foudu sortant de notre musique (ligne 265) puis nous arrêtons la routine de relecture en appelant "mt_end" et nous réinitialisons le timer CIA en appelant la fonction "mt_remove_cia". On restore enfin le système et on quitte le programme.

Voilà c'est fini, notre démo est complète, on peut passer à la conclusion.


Chapitre 12 : Conclusion [retour au plan]

Nous voici arrivés à la fin de notre article sur la programmation du matériel de l'Amiga. J'espère que vous y avez appris des choses intéressantes et, qu'à présent, vous n'hésiterez pas à vous lancer, par exemple, dans une réadaptation de jeu ou dans la réalisation d'une petite démo. De toute façon, comme on dit, c'est en forgeant que l'on devient forgeron. A ce propos, n'hésitez pas, dans un premier temps, à reprendre les différents sources et à essayer d'optimiser le code (il y a de nombreux endroits où cela est possible).

On se retrouvera peut-être prochainement pour un autre dossier sur la programmation de l'AGA ou sur les bases de la 3D.

Bon développement à tous.


[Retour en haut] / [Retour aux articles]