Revenir au plan du site

Intégration du miroir horizontal au moteur

Le besoin le plus courant dans le jeu vidéo est la symétrie d'axe vertical. Comment allons-nous modifier notre moteur d'animation pour la gérer?

D'abord, il faut convenir d'un sens d'affichage dans la structure de nos objets :
struct s_animation
    animation defw
    positionx defw
    positiony defw
    destination defb ; poids fort de la destination hardware
    reverse defb ; sens d'affichage
endstruct

On a déjà le code d'inversion depuis [ L'article sur les miroirs et rotations ].

Modifions notre exécution pour gérer la variable "reverse" supplémentaire :
; IY = objet animation
animate_execute_step
push iy : pop hl
ld e,(hl) : inc hl : ld d,(hl) : dec hl ; current step in 'animation'
ex hl,de : ldi : ldi ; overwrite current step in 'animation'
ld b,#DF : ld c,(hl) : inc hl : out (c),c ; connexion de la bank contenant les données
ld a,(hl) : inc hl : ld xl,a : ld xh,a ; backup du compteur dans XL et XH
ld e,(hl) : inc hl : ld d,(hl) : inc hl : push hl : ex hl,de ; HL=sprite data source
ld d,(iy+s_animation.destination) ; on n'initialise plus E !
ld b,hi(hsp_conversion) ; poids fort pour la table de conversion
ld a,(iy+s_animation.reverse) : dec a : jp z,.unpack_hsp_reverse

Assez peu de changements, on n'initialise plus le poids faible de la destination (registre E) car l'affichage en inverse ne part pas de zéro. Enfin, un dernier test avant la copie va nous diriger soit vers la copie classique, soit vers la copie inversée. Si on voulait gérer plus de transformations, il serait judicieux de placer directement un pointeur de saut à cet endroit et l'intégrer dans l'objet.

Que manque-t'il pour compléter le moteur? Ah oui! La routine d'affichage inverse, avec une lecture des delta modifiée!
;***************************************************************
.unpack_hsp_reverse
;***************************************************************
ld e,15 ; on part de la FIN de la ligne destination
.unpack_hsp_reverse_loop
repeat 8,x
ld a,(hl) : inc l : ld (de),a : dec e
ld c,a : ld a,(bc) : ld (de),a
if x<7 : dec e : else ; on décrémente SAUF sur le dernier pixel écrit
ld a,e : add 31 : ld e,a ; on revient au point de départ, +16 pour être à la fin de la ligne suivante :)
endif
rend
; la dernière comparaison change, on ne teste plus le retour à zéro mais le dépassement de 255
jr nc,.unpack_hsp_reverse_loop
dec l : inc hl
inc d ; next sprite
dec xl ; E still at 15
jr nz,.unpack_hsp_reverse_loop
; DE = next sprite + 15

La routine de copie termine un peu différemment (adresse du sprite suivant+15) mais cela ne va pas changer nos calculs car on récupère l'indice des sprites à modifier avec le poids fort.

Par contre, qui dit copie inversée en symétrie verticale, dit une inversion des deltaX lus, tout simplement une soustraction au lieu d'une addition. C'est la seule différence.
ld a,d : sub #40 : sub xh : ld c,xh
add a : add a : add a : ld xh,#60 : ld xl,a
ld hl,(iy+s_animation.positionx) : ld de,(iy+s_animation.positiony)
pop iy : ld a,c
; IX=info du premier sprite dans l'ASIC
; IY=info de placement des sprites
; HL=x DE=y du premier sprite
; A=nombre de sprites
; SetMetaSprite
; **** d'abord boucler sur les X ************
push af,de,ix : ld de,8
.modifyXreverse
exa
ld c,(iy+0)
ld a,c : add a : sbc a : ld b,a ; faire une valeur 16 bits signées à partir de C
xor a : sbc hl,bc ; modifier le X avec l'inverse du modificateur
ld (ix+0),hl
add ix,de
inc iy
exa : dec a : jr nz,.modifyXreverse
pop ix,hl,af
.modifyYreverse
exa
ld c,(iy+0)
ld a,c : add a : sbc a : ld b,a ; faire une valeur 16 bits signées à partir de C
add hl,bc ; modifier le X
ld (ix+2),hl
add ix,de
inc iy
exa : dec a : jr nz,.modifyYreverse
ret

Avec ce premier jet, Esteban s'affiche bien dans les deux sens...
    ...mais il est centré sur les coordonnées (avant DeltaX) du premier sprite de notre objet


On pourrait centrer automatiquement le sprite mais ce n'est pas obligatoirement ce qu'on souhaite. Il ne faut de toutes façons que modifier la coordonnée de départ, sur toutes les étapes attention! On va modifier notre macro de définition des delta pour prendre en compte le centre de gravité sur le premier delta.
macro animate_push_delta dekx,deky
if animateStepCount==0
    animateStepDeltaX{animateStepCount}={dekx}+animateGravityCenter
else
    animateStepDeltaX{animateStepCount}={dekx}
endif
animateStepDeltaY{animateStepCount}={deky}
animateStepCount+=1
if animateStepCount>animateStepCountTarget : assert 0==1,"Trop d'etapes!" : endif
mend

Notre Esteban s'étale sur 3 sprites en largeur (taille des pixels mode 1) donc le milieu se situe à 1.5 x 32 - 16
1.5 x 32 pour être au milieu des 3 sprites en largeur et 16 en moins car le début d'un sprite se situe sur la gauche et que c'est la taille d'un sprite en résolution mode 2.

On ajoute la définition de notre centre de gravité avant de définir le meta-objet.
animateGravityCenter=1.5*32-16


Et voici notre Esteban centré


Gestion d'une liste d'objets

Au plus simple, plutôt que d'appeler notre routine d'exécution plusieurs fois de suite avec différents objets, on va créer une routine qui peut exécuter une liste d'objets.

confine 34
animate_object_list
defs 34,0 ; 16 objets + 1 terminateur à zéro

animate_execute_list
ld hl,animate_object_list
.reloop
ld e,(hl) : inc l : ld d,(hl) : inc l : ld a,d : or e : ret z
push hl : ld iy,de : ex hl,de : call animate_execute_step.skipParam
pop hl
jr .reloop

; IY = objet animation
animate_execute_step
push iy : pop hl ; déjà fait quand on lit depuis la liste!
.skipParam
ld e,(hl) : inc hl : ld d,(hl) : dec hl ; current step in 'animation'
ex hl,de : ldi : ldi ; overwrite current step in 'animation'
...


On fait deux objets esteban (avec la définition du tag reverse)
struct s_animation,estebanObject,1,estebanNage,300,70,#40,0 ; reverse=0
struct s_animation,estebanObjectbis,1,estebanNage,300,100,#44,1 ; reverse=1


On référence nos deux objets dans la liste
ld hl,estebanObject : ld (animate_object_list),hl
ld hl,estebanObjectbis : ld (animate_object_list+2),hl


Et c'est tout! Il n'y a plus qu'à faire un seul appel au moteur pour afficher nos meta-sprites
call animate_execute_list


Ajouter, enlever un objet de la liste

Est-ce que notre gestion de liste ne serait pas un peu rudimentaire? Je propose de faire quelques fonctions pour ajouter/enlever des objets.

D'abord l'ajout, le plus simple, on cherche un emplacement libre dans la liste et on écrit notre pointeur.
; DE=adresse de l'objet
animate_add_object
ld hl,animate_object_list : ld b,16 ; max 16 objets de 1
.searchSlot
ld a,(hl) : inc l : or (hl) : jr z,.addObject : inc l : djnz .searchSlot : ret ; impossible d'ajouter
.addObject
ld (hl),d : dec l : ld (hl),e ; objet ajouté
ret


La suppression d'un objet est plus touchy, il faut trouver notre objet et aussi trouver le dernier de la liste. Si notre objet est le dernier de la liste alors on efface l'entrée, terminé. Si notre objet n'est pas le dernier de la liste, il faut copier le dernier à l'emplacement de celui qu'on veut effacer et ensuite effacer le dernier de la liste.
; DE=adresse de l'objet
animate_remove_object
ld hl,animate_object_list : ld b,16 ; max 16 objets de 1
.searchSlot
ld a,(hl) : cp e : jr z,.lowMatch : inc l : inc l : djnz .searchSlot : ret ; pas trouvé
.lowMatch
inc l : ld a,(hl) : cp d : jr z,.highMatch : inc l : djnz .searchSlot : ret ; pas trouvé
.highMatch
ld de,hl ; sauvegarde dans DE de l'adresse de l'objet à supprimer dans la liste+1
.findLastObject
inc l : ld a,(hl) : inc l : or (hl) : jr nz,.findLastObject ; on ne compte plus, on doit obligatoirement trouver 0
; HL=adresse du dernier objet vide+1, nous on veut le dernier objet tout court
dec l : dec l ; on revient sur l'objet d'avant qui contient quelque chose
ld a,l : cp e : jr z,.razEntry
ldd : ld a,(hl) : ld (de),a ; on copie le dernier objet de la liste sur celui qu'on enlève
ld (hl),0 : inc l : ld (hl),0 : ret ; et on l'efface de la fin de la liste
.razEntry ld (hl),0 : dec l : ld (hl),0 : ret ; notre objet était le dernier de la liste, on l'efface


Et voilà, notre moteur est paré pour la flexibilité :)

On peut tester que tout fonctionne rapidement.
ld de,estebanObject : call animate_add_object ; ajout du premier
ld de,estebanObjectbis : call animate_add_object ; ajout du deuxième

ld de,estebanObjectbis : call animate_remove_object ; suppression du dernier
ld de,estebanObjectbis : call animate_add_object ; on le rajoute

ld de,estebanObject : call animate_remove_object ; suppression du premier avec un suivant
ld de,estebanObject : call animate_add_object ; et on le rajoute, l'ordre sera inversé dans la liste


Vous pouvez télécharger le [ zip d'exemple ] dans lequel on s'amuse à activer/désactiver les objets les uns à la suite des autres. Toutes les fonctions d'animation du moteur sont regroupées dans un source newAnimate.asm.