Revenir au plan du site

Sauvegarde et restitution du fond d'un sprite clippé

OK, alors on sait sauvegarder le fond avant affichage et aussi restituer le fond avant d'entamer un nouvel affichage. Qu'est-ce qui change avec les sprites clippés?

En fait peu de choses, on a vu que notre routine clippée affichait uniquement dans l'écran, et à y bien regarder, pour chaque sprite clippé, il existe un rectangle unique qui correspond à la version clippée de notre sprite. Il est donc possible d'utiliser les mêmes routines de sauvegarde et restitution que pour les sprites non clippés.
Par contre, comme nous ne voulons pas refaire les calculs de clipping, on va stocker plus d'informations dans notre tableau de sprites.

Ce tableau de sprites à afficher va aussi contenir des informations indépendantes pour la restitution :
- À commencer par la largeur et la hauteur qui peuvent changer.
- On va aussi mettre l'adresse du tampon écran
- L'adresse de départ à l'écran sera la même, on ne va pas la recalculer, on la stocke aussi!

struct sprite
    positionx defw
    positiony defw
    donnees   defw
    sprLargeur defb ; si zéro, liste terminée
    sprHauteur defb
    adresseEcran   defw ; pour la restitution
    resLargeur defb ; pour la restitution ; si zéro, ne pas restituer
    resHauteur defb ; pour la restitution
    adresseSauvegarde defw ; pour la restitution
endstruct

On reprend la routine de sauvegarde du cours [Restituer le fond après le déplacement d'un sprite]
On va tailler dedans pour enlever le calcul d'adresse redondant et aussi préserver quelques informations :
- la hauteur (car on boucle avec un DEC dessus, en fin de routine elle serait perdue).
- HL parce qu'on va afficher le sprite au même endroit qu'on sauvegarde...
- DE parce qu'on enregistre ailleurs que la zone qu'on va lire pour afficher
SauvegarderPortionEcran
; HL=source écran
; DE=tampon de sauvegarde
push de,hl
ld de,(iy+sprite.adresseSauvegarde)
ld a,xh : ld yh,a ; sauvegarde de XH
ld de,(iy+sprite.adresseEcran)
ld hl,(iy+sprite.adresseSauvegarde)
ld b,0
.sauvegardeLignes
ld c,xl ; chargeur la largeur d'une ligne
push hl ; on met l'adresse de début de la ligne de côté
ldir
pop hl ; on récupère l'adresse du début de ligne
; et on calcule le passage à la ligne suivante
ld a,h : add 8 : ld h,a ; ajouter #800 à HL
and #38 ; on teste si on déborde de la bank (passage de page+#3800 à page+#0000)
jr nz,.nextLine ; pas zéro, on est toujours dans le même bloc de lignes
ld a,80  : add l : ld l,a ; on ajoute 80 (largeur d'une ligne en octets) pour passer au bloc suivant
ld a,#C0 : adc h : ld h,a ; et on enlève #4000 (additionner #C000 c'est comme enlever #4000)
.nextLine
dec xh ; notre compteur de lignes
jr nz,.sauvegardeLignes
ld a,yh : ld xh,a ; restitution de XH
pop hl,de
ret


Concernant les restitutions, elles peuvent se faire toutes en même temps, avant de commencer à afficher les nouveaux sprites, on va faire une boucle simple sur notre tableau et voir si il faut restituer ou pas. Et surtout, aucun test de clipping, on sait qu'on est toujours dedans!
RestituerPortionEcranReboucle
ld bc,{sizeof}sprite : add iy,bc ; sprite suivant!
RestituerPortionEcran
ld a,(iy+sprite.sprLargeur) : or a : ret z ; terminé
ld a,(iy+sprite.resLargeur) : or a : ret z ; terminé aussi ^_^
ld xl,a : ld a,(iy+sprite.resHauteur) : ld xh,a
ld hl,(iy+sprite.adresseSauvegarde)
ld de,(iy+sprite.adresseEcran)
ld b,0
.restitueLignes
ld c,xl ; chargeur la largeur d'une ligne
push de ; on met l'adresse de début de la ligne de côté
ldir
pop de ; on récupère l'adresse du début de ligne
; et on calcule le passage à la ligne suivante
ld a,d : add 8 : ld d,a ; ajouter #800 à HL
and #38 ; on teste si on déborde de la bank (passage de page+#3800 à page+#0000)
jr nz,.nextLine ; pas zéro, on est toujours dans le même bloc de lignes
ld a,80  : add e : ld e,a ; on ajoute 80 (largeur d'une ligne en octets) pour passer au bloc suivant
ld a,#C0 : adc d : ld d,a ; et on enlève #4000 (additionner #C000 c'est comme enlever #4000)
.nextLine
dec xh ; notre compteur de lignes
jr nz,.restitueLignes
jp RestituerPortionEcranReboucle


Maintenant il faut rassembler tout ce petit monde dans un seul source et faire un test :)

Pour ce test, nous aurons deux tableaux de sprite (avec 1 sprite) car il faut un tableau par buffer. On commence par restituer le fond si il y a lieu d'être, ensuite on demande l'affichage et une sauvegarde se fait au préalable. C'est vraiment un gros morceau, prenez le temps de le digérer, avec ce source vous avez un moteur d'affichage complet (hors scrollings ;) )

Téléchargez les fichiers [hibou] et [foret] si ce n'est déjà fait
BUILDSNA : BANKSET 0
ORG #38 : EI : RET
ORG #100 : RUN #100

struct sprite
    positionx defw
    positiony defw
    donnees   defw
    sprLargeur defb ; si zéro, liste terminée
    sprHauteur defb
    adresseEcran   defw ; pour la restitution
    resLargeur defb ; pour la restitution (si 0, ne rien faire)
    resHauteur defb ; pour la restitution
    adresseSauvegarde defw ; pour la restitution
endstruct

ld sp,#100 : ei
ld bc,#7F00+%10001100+%00 : out (c),c ; MODE 0
ld hl,palette : ld bc,#7F00
setPalette out (c),c : inc c : inc b : outi : ld a,(hl) : or a : jr nz,setPalette

reboucle
ld iy,tableauTic : call RestituerPortionEcran ; ne fera rien à la première exécution
ld iy,tableauTic : call BougeSprite
ld iy,tableauTic : ld a,#C0 : ld (videoBank),a
call AfficherTableauSprite : call WaitVBL : ld bc,#BC00+12 : out (c),c : ld bc,#BD30 : out (c),c

ld iy,tableauToc : call RestituerPortionEcran ; ne fera rien à la première exécution
ld iy,tableauToc : call BougeSprite
ld iy,tableauToc : ld a,#80 : ld (videoBank),a
call AfficherTableauSprite : call WaitVBL : ld bc,#BC00+12 : out (c),c : ld bc,#BD20 : out (c),c
jr reboucle
;---------------------------------------------------------------------
BougeSprite
;---------------------------------------------------------------------
ld hl,(.positionx) : ld de,(.bougex) : add hl,de : ld (.positionx),hl : ld (iy+sprite.positionx),hl ; bouger et mettre à jour la structure courante
ld hl,(.positiony) : ld de,(.bougey) : add hl,de : ld (.positiony),hl : ld (iy+sprite.positiony),hl ; idem pour le Y
; gérer les changements de direction
ld a,(.bougey) : or a : jr z,.testx
; on bouge en Y, tester le min et le max
ld a,(.positiony) : cp -18 : jp z,.aDroite
cp 184 : jp z,.aGauche
ret
.testx
ld a,(.positionx) : cp #FC : jr z,.enHaut
cp 76 : jr z,.enBas
ret
.aDroite ld hl,0 : ld (.bougey),hl : inc l : ld (.bougex),hl : ret
.aGauche ld hl,0 : ld (.bougey),hl : dec hl : ld (.bougex),hl : ret
.enBas ld hl,0 : ld (.bougex),hl : inc l : inc l : ld (.bougey),hl : ret
.enHaut ld hl,0 : ld (.bougex),hl : dec hl : dec hl : ld (.bougey),hl : ret
.positionx defw 76
.positiony defw 164
.bougex defw 0
.bougey defw 2

tableauTic
defw 0 ; posx
defw 0 ; posy
defw donnees_sprite
defb 9,36 ; dimensions
defw 0
defb 0,0
defw tamponTic
defs {sizeof}sprite ; une structure vide pour arrêter le tableau

tableauToc
defw 0 ; posx
defw 0 ; posy
defw donnees_sprite
defb 9,36 ; dimensions
defw 0
defb 0,0
defw tamponToc
defs {sizeof}sprite ; une structure vide pour arrêter le tableau
;----------------------------------------------------
WaitVBL
ld b,#F5
.loop in a,(c) : rra : jr nc,.loop : ret
;----------------------------------------------------
AfficherTableauSprite
;----------------------------------------------------
.loop
ld (iy+sprite.resLargeur),0 ; la largeur de restitution sera notre TAG qu'il faut restituer
ld hl,(iy+sprite.positionx)
bit 7,h : jr nz,.testGauche ; si c'est négatif on fait le test de gauche
; on teste le débordement à droite (80 octets en configuration standard)
ld de,80 : or a : sbc hl,de
jp m,.hautBas
jr .horsEcran
.testGauche
ld a,(iy+sprite.sprlargeur) : add l : ld l,a : ld a,h : adc 0
jp m,.horsEcran ; négatif hors écran, reste à tester le zéro
ld a,l : or a : jr z,.horsEcran ; on fait un raccourci de calcul car on partait d'une position négative
; quasiment le même code de tester le débordement en vertical
.hautBas
ld hl,(iy+sprite.positiony)
bit 7,h : jr nz,.testHaut ; si c'est négatif on fait le test du haut
; on teste le débordement en bas de l'écran de 200 lignes
ld de,200 : or a : sbc hl,de
jp m,.affiche
jr .horsEcran
.testHaut
ld a,(iy+sprite.sprhauteur) : add l : ld l,a : ld a,h : adc 0
jp m,.horsEcran ; négatif hors écran, reste à tester le zéro
ld a,l : or a : jr z,.horsEcran ; on fait un raccourci de calcul car on partait d'une position négative
.affiche
ld b,#C0 : videoBank=$-1 ; bank vidéo par défaut
ld c,(iy+sprite.positionx) ; on peut ne lire que 16 bits à la même adresse, merci le little endian
ld hl,(iy+sprite.positiony)
ld de,(iy+sprite.donnees)
ld a,(iy+sprite.sprlargeur) : ld xl,a
ld a,(iy+sprite.sprhauteur) : ld xh,a
push iy ; car notre routine de sprite masqué utilise YL ...
call AfficheSprite
pop iy
.horsEcran
; ajouter ici une condition de bouclage, par exemple sur les données...
ld de,{sizeof}sprite : add iy,de
ld a,(iy+sprite.sprlargeur) : or a ; tester la largeur du prochain sprite
jp nz,.loop ; on reboucle si le prochaine sprite n'a pas une largeur nulle
ret
;----------------------------------------------------
AfficheSprite
;----------------------------------------------------
; BC=page + coordonnée X (0-79) / HL=coordonnée Y (0-199) / DE=adresse des données du sprite
; XL=largeur du sprite en octets / XH=hauteur du sprite en nombre de lignes
bit 7,h : jr z,.pasClipHaut ; si le Y est positif ou nul, pas de clipping en haut
; XH est la hauteur affichable, on soustrait le Y négatif
push hl : ld a,xh : add l : ld l,a : ld a,h : adc 0 : ld h,a : ld a,l : ld xh,a : pop hl
; vu qu'on saute des lignes, il faut avancer le pointeur de données
ld a,l : ex hl,de : ld d,0 : ld e,xl
.sauteLignes add hl,de : inc a : jr nz,.sauteLignes ; +simple qu'une multiplication
ex hl,de ; remettre le pointeur dans DE
ld hl,0 ; tout est corrigé, on dit qu'on part maintenant de la ligne 0
.pasClipHaut
; pour rester sur des comparaisons 8 bits, on peut soustraire sprHauteur à hauteur et comparer à sprY
ld a,200 : sub xh : cp l : jr nc,.pasClipBas ;
ld a,200 : sub l : ld xh,a ; nouvelle hauteur +petite
.pasClipBas
xor a ; offsetDebut par défaut
bit 7,c : jr z,.pasClipGauche
ld a,c : add xl : ld xl,a ; nouvelle largeur
ld a,c : neg ; nouvel offsetDebut
ld c,0 ; nouveau X en zéro
.pasClipGauche
ld (offsetDebut),a ; soit zéro, soit celui calculé lors du clipping
ld a,c : add xl : cp 81 ; largeur+1 rapport à la comparaison souhaitée
ld a,0 : jr c,.pasClipDroite ; A=0 au cas où on n'ait pas de clipping, pour reset offsetFin
ld a,xl : exa ; on met la largeur du sprite de côté
ld a,80 : sub c : ld xl,a ; nouvelle largeur de sprite
exa ; on récupère la largeur d'origine
sub xl
.pasClipDroite
ld (offsetFin),a

call CalculeAdressePixel
;----------------------------------------------------------------------------------------------------
; ici le clipping est terminé, on a des coordonnées dans l'écran et des tailles qui ne débordent pas
; IY n'est pas encore altéré, on peut inscire nos infos de sauvegarde du fond
ld (iy+sprite.adresseEcran),hl
ld a,xl : ld (iy+sprite.resLargeur),a
ld a,xh : ld (iy+sprite.resHauteur),a
call SauvegarderPortionEcran
;----------------------------------------------------------------------------------------------------
; HL=destination écran
; DE=toujours l'adresse source des données
ld bc,hl ; on utilise BC comme destination écran

ld h,hi(tableTransparence)
.afficheLignes
ld a,xl : ld yl,a ; chargeur la largeur d'une ligne dans YL
push bc ; on met l'adresse de début de la ligne de côté

ld a,e : add #12 : offsetDebut=$-1 : ld e,a : ld a,d : adc 0 : ld d,a
.pixelMasque
ld a,(de) ; lire le sprite
ld l,a ; octet du sprite dans L
ld a,(bc) ; on récupère l'octet de l'écran
and (hl) ; on applique le masque
or l ; on fusionne avec la donnée du sprite
ld (bc),a ; on remet dans l'écran
inc bc
inc de ; et on incrémente data+ecran
dec yl
jr nz,.pixelMasque
ld a,e : add #12 : offsetFin=$-1 : ld e,a : ld a,d : adc 0 : ld d,a

pop bc ; on récupère l'adresse du début de ligne
; et on calcule le passage à la ligne suivante
; notre routine de passage à la ligne suivante, adaptée pour BC
ld a,b : add 8 : ld b,a ; ajouter #800 à DE
and #38 ; on teste si on déborde de la bank (passage de page+#3800 à page+#0000)
jr nz,.nextLine ; pas zéro, on est toujours dans le même bloc de lignes
ld a,80  : add c : ld c,a ; on ajoute 80 (largeur d'une ligne en octets) pour passer au bloc suivant
ld a,#C0 : adc b : ld b,a ; et on enlève #4000 (additionner #C000 c'est comme enlever #4000)
.nextLine
dec xh ; notre compteur de lignes
jr nz,.afficheLignes
ret
;----------------------------------------------------
SauvegarderPortionEcran
;----------------------------------------------------
; HL=source écran
; DE=tampon de sauvegarde
push de,hl
ld de,(iy+sprite.adresseSauvegarde)
ld a,xh : ld yh,a ; sauvegarde de XH
ld b,0
.sauvegardeLignes
ld c,xl ; chargeur la largeur d'une ligne
push hl ; on met l'adresse de début de la ligne de côté
ldir
pop hl ; on récupère l'adresse du début de ligne
; et on calcule le passage à la ligne suivante
ld a,h : add 8 : ld h,a ; ajouter #800 à HL
and #38 ; on teste si on déborde de la bank (passage de page+#3800 à page+#0000)
jr nz,.nextLine ; pas zéro, on est toujours dans le même bloc de lignes
ld a,80  : add l : ld l,a ; on ajoute 80 (largeur d'une ligne en octets) pour passer au bloc suivant
ld a,#C0 : adc h : ld h,a ; et on enlève #4000 (additionner #C000 c'est comme enlever #4000)
.nextLine
dec xh ; notre compteur de lignes
jr nz,.sauvegardeLignes
ld a,yh : ld xh,a ; restitution de XH
pop hl,de
ret
;----------------------------------------------------
RestituerPortionEcranReboucle
;----------------------------------------------------
ld bc,{sizeof}sprite : add iy,bc ; sprite suivant!
;----------------------------------------------------
RestituerPortionEcran
;----------------------------------------------------
ld a,(iy+sprite.sprLargeur) : or a : ret z ; terminé
ld a,(iy+sprite.resLargeur) : or a : ret z ; terminé aussi ^_^
ld xl,a : ld a,(iy+sprite.resHauteur) : ld xh,a
ld hl,(iy+sprite.adresseSauvegarde)
ld de,(iy+sprite.adresseEcran)
ld b,0
.restitueLignes
ld c,xl ; chargeur la largeur d'une ligne
push de ; on met l'adresse de début de la ligne de côté
ldir
pop de ; on récupère l'adresse du début de ligne
; et on calcule le passage à la ligne suivante
ld a,d : add 8 : ld d,a ; ajouter #800 à HL
and #38 ; on teste si on déborde de la bank (passage de page+#3800 à page+#0000)
jr nz,.nextLine ; pas zéro, on est toujours dans le même bloc de lignes
ld a,80  : add e : ld e,a ; on ajoute 80 (largeur d'une ligne en octets) pour passer au bloc suivant
ld a,#C0 : adc d : ld d,a ; et on enlève #4000 (additionner #C000 c'est comme enlever #4000)
.nextLine
dec xh ; notre compteur de lignes
jr nz,.restitueLignes
jp RestituerPortionEcranReboucle
;----------------------------------------------------
CalculeAdressePixel
;----------------------------------------------------
; B=page vidéo #00, #40, #80 ou #C0
; C=coordonnée X (0-79)
; HL=coordonnée Y (0-199)
; adresse de la ligne dans HL en résultat
add hl,hl ; adresses 16 bits, il faut indexer de 2 en 2
ld a,lo(tableau) : add l : ld l,a : ld a,h : adc hi(tableau): ld h,a
ld a,(hl) : inc hl
ld h,(hl) : ld l,a
add hl,bc ; ajouter la position X en octets et la page!
ret
;-------------------
adresse_ecran=#0000
largeur_ecran=80
;-------------------
tableau
;-------------------
repeat 25
repeat 8
defw adresse_ecran
adresse_ecran+=#800
rend
adresse_ecran+=largeur_ecran
adresse_ecran-=#4000
rend


align 256
;-------------------
tableTransparence
;-------------------
repeat 256,x
px=x-1 ; car le compteur va par défaut de 1 à 256 et non 0 à 255
masque=0
if (px & (128|32|8|2))==0
    masque|=128|32|8|2 ; pour cet octet on conservera les données écran
endif
if (px & (64|16|4|1))==0
    masque|=64|16|4|1 ; pour cet octet on conservera les données écran
endif
defb masque
rend

palette defb #4D,#54,#56,#5C,#46,#5E,#40,#47,#43,#4E,#4B,#4C,0 ; Rose en zéro
donnees_sprite incbin 'hibouZero.bin'
tamponTic defs 9*36
tamponToc defs 9*36
org #8000 : incbin 'foretZero.bin'
org #C000 : incbin 'foretZero.bin'

Comme d'habitude, la petite démo visuelle avec en bas nos deux tampons de sauvegarde du fond. On remarque que lorsque le sprite se déplace à l'horizontale, il consomme moins d'octets (le bas se fige) et lorsqu'il longe les bords gauche/droite, il occupe plus de place mais moins que la taille totale d'un sprite.