Ok, cette partie est sans doute la plus complexe de notre remake, pas forcément au niveau gestion du hardware mais surtout au niveau algo, je vais la découper en deux parties, gestion du gamepad avec analyse des mouvements et gestions des animations du joueur.
Commençons par ouvrir le fichier "mk_03.s", là encore on repart du fichier précédant et on ajoute les constantes pour la gestion des mouvements (lignes 190 à 212). Vous pouvez voir que l'on a attribué un offset pour chaque animation du joueur, il va nous servir à déterminer l'animation à jouer par rapport à une table des animations.
Mais avant celà il faut commencer par tester le joystick afin de déterminer le
mouvement fait par le joueur. Lorsqu'il est sorti sur Amiga, Mortal Kombat a souffert
d'un gros handicap comparé à sa version arcade, c'est le manque de coups différent
qui nous étaient proposés, ceci étant principalement du au fait qu'à l'époque les
joysticks ne comportaient qu'un seul, au mieux deux, boutons, il fallait donc faire avec.
Heuresement, nous avons, depuis la sortie de la CD32, eu droit à un gamepad avec pas moins
de 7 boutons, je vous proposes donc d'en profiter et pour celà nous allons ajouter
une fonction de test du gamepad. Ouvrez le fichier "Input.s", vous remarquerez
de suite que nous avons ajouté quelques constantes pour la gestion du gamepad ainsi
qu'une fonction "CheckGamepad1" (lignes 133 à 173) qui controle
le gamepad branché sur le port 1 et nous retourne son état dans d0
.
Je ne vais pas détailler cette fonction, elle vient du kit de dev de la CD32, sachez
simplement que les boutons du gamepad agissent un peu comme un potard et qu'en fait
ils correspondent à un certain seuil électrique. Le principal c'est que celà fonctionne
(testé sur un vrai hardware). Pour notre remake nous allons utiliser quasiment tous
les boutons du gamepad (autant se faire plaisir), le bouton rouge correspondra
au coup de poing haut, le jaune au coup de poing central, le vert au coup de pied haut,
le bleu au coup de pied central et le rewind au blocage. La croix servira évidemment
à déplacer le sprite du joueur.
La position du gamepad sera controlé dans la VBL mais pas à chaque VBL, en effet,
il est inutile de tester un changement de position 50 fois par secondes car même si
vous êtes très rapide il me semble juste impossible de faire 50 mouvements différents
par seconde, nous allons donc tempérer ce controle en le réalisant que toutes les 8 VBL
(c'est la constante MOVE_CHECK
qui fixe cette valeur, vous pouvez la modifier à loisir).
Celà vous laisse le loisir de changer 6 fois de positions par seconde, ça reste largement
acceptable. Cette temporisation a un autre but, pour gérer les enchainements de coups (pour
les coups spéciaux ou les finish) nous allons utiliser une pile de coups joués (ligne 1504),
ainsi nous pourrons simplement tester si un pattern de coups à été effectué par le joueur
pour déclencher l'animation correspondante. Si nous testions le gamepad à chaque VBL celà
voudrait dire que pour un coup spécial de 4 mouvements il faudrait les enchainer en 4 VBL,
soit en gros 80ms, autant dire c'est juste impossible, en réduisant la fréquence de
test celà laisse au joueur 8 fois plus de temps pour exécuter son enchainement, soit ici
640ms ce qui semble plus réaliste au final. Notre VBL se contente donc de vérifier
la temporisation et d'appeler la fonction "CheckGamepadPlayer1" qu'une fois
sur 8 (lignes 1237 à 1243)).
La fonction "CheckGamepadPlayer1" (lignes 1250 à 1273) commence par appeler la fonction de test du gamepad puis charge l'adresse de la pile des coups. On regarde ensuite si le joueur a changé de position depuis le dernier test et on stocke le résultat dans la variable "Player1SwitchMove", on fait celà car dans le jeu si le joueur reste appuyé sur un bouton de coup il ne faut pas que l'on rejoue le coup à chaque controle mais uniquement la première fois, cette variable va nous aider à tester cette situation. Il faut ensuite empiler la nouvelle position sur la pile des coups joués (lignes 1258 à 1263). On peut, à présent, déterminer le prochain coup du joueur. Pour celà la bonne vieille technique de la table de coups sera utilisée, celà nous évitera un long process d'enchainement de tests qui risque de vite devenir ingérable pour peu que l'on souhaite ajouter de nouveau coups. Donc nous créons une table "Player1MoveTable" qui va contenir deux données pour chaque mouvement possible du joueur, la première étant la position du gamepad et la seconde la fonction à appeler pour gérer le mouvement du joueur. Notre fonction va parcourir cette table et tester si la dernière position du gamepad correspond à une position décrite dans la table (lignes 1266 à 1270), si c'est le cas on appel la fonction associée (ligne 1271), si on ne trouve pas de position correspondante alors on reste sur la position précédante. Il faut tout de même faire attention à ce que cette table ne soit pas trop longue pour rester sur un temps de traitement raisonnable.
Je vais détailler deux fonctions représentatives de gestion des mouvements du joueur. Tout d'abord la fonction "P1MoveLeft" (lignes 1280 à 1290) qui est exécutée lorsque le joueur déplace le gamepad vers la gauche, pour ce mouvement nous devons gérer deux cas, un appui simple effectuera un déplacement rapide à gauche alors qu'un appui prolongé jouera une séquence complète de déplacement. Il faut donc tester au départ si nous ne sommes pas déjà en train de joueur l'animation de déplacement vers la gauche, si tel est le cas il faut jouer la séquence longue, sinon on joue la séquence rapide. Le mouvement en cours est stocké dans la variable "Player1CurrentAnim" par la routine d'animation du joueur que nous verrons plus tard, nous testons donc si il correspond à un mouvement de déplacement vers la gauche et en fonction nous alimentons la variable "Player1NextAnim" qui correspond à la prochaine animation à jouer avec la bonne valeur de déplacement. Voyons à présent la fonction "P1HighPunch" (lignes 1327 à 1335) qui gère le coup de poing haut, comme je l'expliquais, il ne faut pas que l'animation se joue en boucle si l'on reste appuyé sur le bouton du gamepad, on commence donc par tester si le joueur à changé de position depuis le dernier controle (ligne 1328) à l'aide de la variable "Player1SwitchMove", si c'est le cas alors nous lançons l'animation du coup de poing haut, sinon on lance l'animation de la position d'attente, simple non.
J'espère que vous avez bien saisi le principe de la gestion du gamepad, il faut voir que ce traitement est totalement dissocié de la gestion des animations du joueur, en effet les positions du gamepad sont quasiment les mêmes quelque soit le personnage sélectionné (à l'exception des coups spéciaux) alors que les animations sont spécifique à chaque personnage, il faut donc bien séparer les deux traitements si on ne veux pas avoir à coder une gestion du gamepad par personnage du jeu, ce qui deviendrai vite ingérable.
Dans un souci de clareté j'ai choisi d'isoler l'ensemble des données d'animation d'un
personnage dans un fichier à part, ouvrez le fichier "cage_anim.s" nous allons
l'étudier. En premier lieu nous trouvons la table des animation du personnage qui regroupe
simplement les adresses des différentes animation du personnage à jouer en fonction des
mouvements du joueur dans l'ordre définis par les constantes MOVE_
(lignes 8 à 28).
Puis nous avons les descriptions de chacune des animations du personnage, elles sont toutes
basées sur la même structure, étudions la première "CageWaitAnimTable" (lignes 30 à 54).
Le premier mot indique le nombre de frames de l'animation, le deuxième mot indique si
l'animation peut être interrompue par une autre animation (par exemple lorsque le
joueur se déplace vers la gauche il doit pouvoir à tout moment déclencher un coup de pied) enfin
le long mot suivant contient l'adresse du son associé à l'animation (nous gèrerons celà dans
la prochain chapitre), 0 voulant dire pas de son associé. Ensuite nous retrouvons la
structure classique de l'animation, un mot pour indiquer le nombre de VBL que doit durer la
frame, puis l'adresse du sprite à afficher et, petite nouveauté, les offset de déplacement
du sprite au cours de l'animation. Il suffit de répeter ce shéma pour chaque animation du joueur
et c'est réglé. Pour terminer nous avons notre banque de sprites qui s'est un peu étoffée depuis
le dernier chapitre. Idéalement, si nous devions réellement réaliser un jeu il nous faudrait
des outils pour gérer ces structures de données plutôt que de tout créer à la main.
Passons maintenant au coeur du programme, la fonction "AnimatePlayer1Sprite" (lignes 856 à 899) qui n'est pas nouvelle mais que l'on a pas mal amélioré depuis le dernier chapitre. Cette dernière est toujours appelée à chaque VBL afin d'avoir une animation fluide du personnage. On commence donc de façon classique par tester si l'on doit afficher la frame suivante de l'animation (lignes 857 et 858), puis on regarde si l'animation est stoppable (on peut donc interrompre une animation entre deux frames), si c'est le cas il faut regarder si on doit jouer une nouvelle animation (lignes 859 à 863) ou continuer l'animation en cours en passant à la frame suivante et en s'assurant que l'on est pas arrivé à la fin de l'animation (lignes 865 et 866). Pour déterminer la prochaine animation à jouer on récupère la valeur de la variable "Player1NextAnim" puis on recherche, grace à la table des animations "Player1AnimTable", l'adresse de l'animation à jouer (lignes 869 à 871). On peut alors initialiser nos variables avec les données de l'animation, d'abord le compteur de frames, puis le flag indiquant si l'animation est stoppable et enfin le son à jouer avec l'animation (lignes 873 à 875). On sauve ensuite le pointeur pour le traitement suivant. Si on doit jouer un son on appel la fonction "PlaySoundFx" (on implémentera cette fonction au chapitre suivant). On déroule ensuite le traitement classique de gestion d'une nouvelle frame avec en plus la récupération de l'offset de déplacement du sprite que l'on va stocker dans la variable "Player1Movement" et on recopie le nouveau sprite dans ses buffers (lignes 881 à 890). On utilise ensuite la variable "Player1Movement" pour calculer la nouvelle position du sprite en appelant la fonction "CalculPlayer1Position" (lignes 990 à 1008). On se contente en fait d'ajouter les offset X et Y à la position courante de notre sprite que l'on a stocké dans la variable "Player1Position", on en profite aussi pour tester si notre sprite a atteind la limite gauche ou droite de l'écran, et dans ce cas on en profites pour faire scroller l'écran en appelant soit "MoveBGLeft", soit "MoveBGRight" et finalement on sauvegarde la nouvelle position du sprite dans sa variable. La dernière étape de notre fonction d'animation consiste à mettre à jour les mots de controle de nos sprite en fonction de leurs nouvelles positions et c'est le rôle de la fonction "SetupPlayer1SpriteControl" qui n'a pas changé depuis le chapitre précédant.
C'en est fini pour la gestion de nos animations, vous pouvez assembler le fichier et l'exécuter, n'oubliez pas votre gamepad et on se retrouve pour le chapitre suivant.
Tiens prends ça !
Et ça !