GBA librairie v0.1 pour TI-82

 

Ce tutorial s'adresse à ceux qui ont déjà les bases de programmation en assembleur pour TI et qui souhaitent écrire des jeux type game-boy de manière rapide et claire. Les outils développés dans cette librairie sont puissants mais vous laisseront quand même une grande liberté qui, je pense, sera suffisante pour créer la plupart de vos jeux. J'utiliserai tout au long de ce tutorial des exemples réalisés pour la shell crash, mais rien ne vous empêche de les transposer sur une autre shell ou même sur une rom TI-82 plus ancienne (comme la rom 19.0): en effet, GBA.82p n'utilise aucune fonction spécifique à crash ni aucun ROM-CALL. Enfin, si vous avez des questions ou des suggestions, n'hésitez pas à me contacter.

 

I) Introduction
1) Relier votre programme à la librairie
2) A propos des variables
II) Utilisation des cartes mosaïques
1) Principe
2) Affichage d'une carte
3) scroll multidirectionnel
4) changer la taille du cadre
III) Gestion des sprites
1) Afficher un sprite à l'écran
2) Organiser vos sprites dans votre programme
3) Inclure des sprites dans vos cartes
4) Chargement d'un sprite dans la table
5) Supprimer un sprite
IV)    Compléments
1) Quelques routines et variables supplémentaires
2) Récapitulatif de toutes les fonctions de GBA


I) Introduction

1) Relier votre programme à la libraire

Comme vous l'aurez sans doute compris,GBA s'utilise en fichier externe: les routines ne sont pas à recopier dans votre code source, elles ont été rassemblées et compilées pour être directement appelées depuis votre propre programme. Le problème est que les variables de la TI-82 changent souvent d'adresse; lorsque vous supprimez un réel ou une liste par exemple, les autres variables sont automatiquement déplacées pour prévenir la défragmentation. Pour appeler une routine d'un programme depuis un autre, il faut bien connaître l'adresse de cette routine dans la mémoire! Une solution aurait été de fixer l'adresse du programme GBA.82p avec un fichier back-up, comme le font la plupart des shells (pour plus d'information, lisez le document 82-hack de Mattias Lindqvist & Dines Justesen, disponible sur ticalc.org), mais cela aurait posé des problèmes d'incompatibilité avec les différentes shells ainsi que des problèmes de portage sur les autres TI... J'ai alors choisi d'écrire une routine dans un fichier #include qui va chercher l'adresse du programme GBA.82p via la VAT, et qui actualise toutes les adresses des routines en fonction de celle du programme. Cette routine s'appelle Find_gba, se trouve dans le fichier #include Findgba.inc, et définit aussi un tas de variables utilisées par GBA.Vous l'incluerez à la fin de votre code source. En pratique, voici ce que ça donne:



#include "crash82.inc"

.org START_ADDR					; en-tête normal
.db "Premiers pas",0

	call Find_gba				; actualise toutes les adresses des routines


	;; votre programme
	;; ...
	;; ...


#include "Findgba.inc"
.end

 

Que se passe-t-il si le programme GBA.82p est introuvable, si vous avez oublié de l'envoyer à votre calculatrice? Dans ce cas, le carry flag vaudra 1. On peut donc améliorer le programme précédent:

 

#include "crash82.inc"

.org START_ADDR					; en-tête normal
.db "Premiers pas",0

	call Find_gba				; actualise toutes les adresses des routines
	jr nc,GBA_ok				; s'il trouve le fichier (carry flag=0),
	ld hl,Texte_erreur			; il passe à la suite du programme
	ROM_CALL(D_ZT_STR)
Pause:
	call GET_KEY
	cp G_ENTER
	jr nz,Pause
	ret
Texte_erreur:
.db "Erreur 404: not found",0	; :)


GBA_ok:
	;; votre programme
	;; ...
	;; ...


#include "Findgba.inc"
.end

Et voilà! Vous savez à présent comment relier votre programme avec GBA.82p.

 

2) A propos des variables

Toutes les variables utilisées par le programme GBA se trouvent dans la 2e mémoire texte (TEXT_MEM2). On ne peut en effet rien stocker dans le programme GBA.82p car il faudrait des variables pour indiquer où se trouvent ces variables :) . Les 32 premiers octets ( de TEXT_MEM2 jusqu'à TEXT_MEM2+31) sont des paramètres quelconques. Les 54 octets suivants (de TEXT_MEM2+32 jusqu'à TEXT_MEM2+88) sont une table de jump; lorsque vous appelez une routine de GBA, le programme va sur cette table et est dirigé vers l'adresse de la routine correspondante, ne modifiez surtout pas la valeur de ces 54 octets!

 

II) Les cartes mosaïques

1) Principe

La plupart des jeux en deux dimension utilisent le principe des cartes mosaïques : l'image qui sera affichée à l'écran est divisé en petits carrés de taille fixe (tile en anglais) auxquels sont attribués des numéros. Les cartes sont en fait des matrices qui contiennent les numéros de ces petits carrés. Voici un exemple tiré du jeu Zelda II: The Adventure Of Link (assez vieux certes, mais culte!) :

 

On distingue 6 tiles différents (de taille 16*16 pixels):
- un pour la prairie
- un pour la route
- un pour les forêts
- un pour les montagnes
- un pour le lac
- un pour le temple

On veut maintenant faire la même chose sur TI-82...

On commence alors par dessiner les tiles, de dimension 8*8 pixels...

... puis on les transforme en données binaires que l'on inclut dans notre programme. Les numéros associés aux tiles sont ceux de leur odre dans la liste, en commençant par zéro:

Tiles:

.db %10000000		; tile numéro 0 (prairie)
.db %10000100
.db %00010000
.db %01000001
.db %00000000
.db %00100000
.db %00000010
.db %00000000

.db %00000000		; tile numéro 1 (route)
.db %00000000
.db %00000000
.db %00000000
.db %00000000
.db %00000000
.db %00000000
.db %00000000

.db %00010000		; tile numéro 2 (forêt)
.db %00110000
.db %00101000
.db %01100000
.db %01010100
.db %11000000
.db %11111110
.db %00010000

.db %00000000		; tile numéro 3 (montagne)
.db %00000000
.db %00001000
.db %00010100
.db %00100110
.db %01010010
.db %11011001
.db %10001000

.db %11111010		; tile numéro 4 (étendue d'eau)
.db %11011111
.db %10101101
.db %11111010
.db %11101111
.db %11010111
.db %11111111
.db %11111101

.db %11111111		; tile numéro 5 (temple)
.db %01011110
.db %00101100
.db %00101100
.db %00101100
.db %00101100
.db %01111110
.db %10011111

Ces numéros seront codés sur un octet, ce qui veut dire que vous pouvez fabriquer 256 tiles différents pour une même carte, ce qui doit être largement suffisant. Mais rien ne vous empêche d'utiliser plusieurs listes de tiles dans votre jeu, par exemple créer une table qui indique, pour chaque carte, un liste spécifique de tiles .
On écrit ensuite un tableau qui représente la carte, où chaque numéro correspond au tile qui lui est associé. En reprenant la carte de tout à l'heure on obtient ceci:

Carte:

.db 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,0,0
.db 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,1,1,1
.db 3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
.db 3,3,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1
.db 3,3,0,0,0,0,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0
.db 3,3,0,0,0,0,0,0,2,2,2,2,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0
.db 3,0,0,0,0,0,0,0,0,0,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0
.db 3,0,0,0,0,0,0,0,0,4,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,1,1,0,0,0,2
.db 0,0,0,0,0,0,0,0,4,4,4,4,1,1,1,4,4,4,4,0,0,0,0,0,1,1,0,0,0,2,2
.db 0,0,0,0,0,0,0,0,4,4,4,4,1,5,1,1,1,1,1,1,1,1,1,1,1,0,0,0,2,2,2
.db 3,3,0,0,0,0,0,0,4,4,4,4,1,1,1,4,4,4,4,0,0,0,0,0,0,0,0,0,2,2,2
.db 3,3,3,0,0,0,0,0,4,4,4,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,2,2,2,2
.db 3,3,3,0,0,0,0,0,0,4,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,2,2,2,2,2
.db 3,3,3,3,0,0,0,0,0,0,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,3
.db 3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,3,3,3,3,0,0,0,0,0,0,2,3,3,3,3,3
.db 3,3,3,3,3,0,0,0,0,0,0,0,3,3,3,3,3,3,3,3,3,0,0,0,3,3,3,3,3,3,3

2) Affichage d'une carte

D'abord, il faut indiquer à GBA l'adresse des tiles et celle de la carte; pour cela il suffit de stocker ces adresses respectivement dans les variables tiles_addr et map_addr:

 

	; ...
	ld hl,Carte				; label pointant sur la carte
	ld (MAP_ADDR),hl
	ld hl,Tiles				; label pointant sur les tiles
	ld (TILES_ADDR),hl
	; ...

 

Il faut aussi stocker le nombre de lignes et de colonnes que comporte votre tableau (les dimensions de la carte), dans les variables map_height et map_width.

 

	; ...
	ld bc,16
	ld (MAP_HEIGHT),bc
	ld bc,31
	ld (MAP_WIDTH),bc
	; ...

 

Maintenant que GBA possède toutes les informations techniques sur votre carte, il suffit d'appeler la routine gba_initmap pour afficher la carte dans l'APD BUFFER. Vous pouvez écrire une routine qui copie le contenu de l'APD_BUF dans le GRAPH_MEM (avec l'instruction ldir), mais je vous conseille pour le moment d'utiliser la routine gba_restoremap qui possède quelques propriétés expliquées dans le paragraphe 4).

Vous obtenez ceci:

(voir Exemple1.Asm)

Vous remarquerez que c'est le haut gauche de la carte qui est affiché. Pour changer cela, il existe deux variables de 16 bits, scroll_x et scroll_y, qui définissent le nombre de tiles entiers dépassés par l'écran respectivement vers la gauche et vers le haut. Initialement, ces deux variables valent 0. Changez ces valeurs avant d'appeler la routine gba_initmap pour comprendre comment ça marche. Bien sûr si vous mettez scroll_x=3541 par exemple, il y aura un bug d'affichage, mais la calc ne plantera pas.


3) Scroll multidirectionnel

Après avoir affiché votre carte comme précédement, vous appellerez les routines gba_scroll_right, gba_scroll_left, gba_scroll_down et gba_scroll_up pour effectuer un scroll vers la droite, vers la gauche, vers le bas et vers le haut:

(Voir Exemple2.asm)

Magique, non? Si le scroll est impossible, c'est-à-dire qu'on a atteint les limites de la carte, le carry flag vaudra 1, mais vous n'aurez pas à vous en soucier si vous suivez mes conseils dans la rubrique compléments.

 

4) Changer la taille du cadre

Dans certain jeux, il arrive que l'on veuille laisser une portion de l'écran vide, pour pouvoir dessiner une barre de vie, donner le score, etc. Modifier les routines de GBA pour qu'elles prennent en compte un cadre variable aurait été trop compliqué, et aurait surtout beaucoup ralenti le programme. Le plus simple est certainement de copier dans le GRAPH_MEM uniquement la portion d'image qui nous intéresse, et c'est là qu'intervient la routine gba_restoremap: celle-ci dépend des paramètres screen_width et screen_height, qui donnent la largeur et la hauteur en octet de la portion de l'image qui sera copié de l'APD_BUF dans le GRAPH_MEM. Ces valeures sont initialement à 12 et à 8 (pour une image de 96*64 pixels). Voici ce qu'on obtient en mettant screen_width=4 et screen_height=4 :

De plus, la fenêtre de l'image est par défaut affichée en haut à gauche de l'écran. Il est possible de changer cela : le paramètre zero_point donne l'adresse du pixel le plus en haut à gauche de l'image, moins la constante GRAPH_MEM. Si on appelle (x,y) les coordonnées de ce pixel (avec x multiple de 8) à l'écran, son adresse est GRAPH_MEM+12*y+x/8, c'est-à-dire zero_point = 12*y+x/8. Cette valeur est initialement à 0 (la fenêtre est affichée à partir de GRAPH_MEM). Par exemple, pour afficher l'image aux coordonnées (16,3), il suffit de mettre zero_point= 3*12+16/8 = 38. On obtient:

La routine gba_restoremap n'est donc pas nécessaire dans tous les cas : je ne l'ai écrite que pour donner le cas général ; je vous conseille même d'écrire votre propre routine adaptée à votre jeu, qui sera bien plus rapide.

 

III) Gestion des sprites

Maintenant que vous savez manier les cartes mosaïques, voyons comment gérer l'affichage des sprites. Nous verrons enfin comment les inclure directement dans vos cartes.

1) Afficher un sprite à l'écran

Rappel sur l'utilisation des masques : Qu'est-ce qu'un masque? Les routines "simples" d'affichage de sprites utilisent l'opérateur OR pour copier l'image vers un endroit du graph_mem. Ainsi si un bit de l'image vaut 1, il donnera un pixel noir. Mais s'il vaut 0, il donnera soit un pixel noir, soit un pixel blanc. Le sprite affiché est donc transparent. Pour régler ce problème, on va créer une autre image, appelée masque, qui a pour but d'effacer les pixels avant l'affichage de l'image. Ce masque est copié avec l'opérateur AND: si un bit du masque vaut 1, on obtiendra soit un pixel noir soit un pixel blanc. S'il vaut 0, on obtiendra toujours un pixel blanc.Un tableau vaut mieux qu'un long discours:

pixel voulu bit de l'image

bit du masque

blanc 0 0
noir 1 peu importe
transparent 0 1

 

Par exemple, voici un sprite (de dimension 16*28 pixels) tiré de Super Mario World (les pixels gris représentent les pixels transparents) :

A partir de là, on réalise l'image du sprite:

puis son masque:


Afficher un sprite avec GBA : Chez GBA, le masque et l'image sont mêlés : on crée un label où l'on met le premier octet du masque, puis le premier octet de l'image, le second octet du masque, puis le second octet de l'image, etc. En bitmap, ça donnerait ceci:

On convertit ensuite ce dessin en données binaires que l'on copiera directement dans notre programme (je vous conseille pour cela d'utiliser l'excellent programme "Bitmap to asm", disponible sur ticalc.org):

Mario_sprite:
	.db %11111110,%00000001,%00001111,%11110000
	.db %11111000,%00000110,%00000111,%00001000
	.db %11110000,%00001000,%00000111,%00001000
	.db %11100000,%00010001,%00000011,%11111100
	.db %11000000,%00100111,%00000001,%11111110
	.db %10000000,%01001111,%00000001,%11111110
	.db %10000000,%01001100,%00000011,%00001100
	.db %10000000,%01011000,%00000011,%10100000
	.db %00000000,%11011000,%00000011,%10100100
	.db %00000000,%11011100,%00000011,%00000100
	.db %10000000,%01001001,%00000011,%00000100
	.db %10000000,%01100011,%00000001,%11111110
	.db %11000000,%00110000,%00000011,%11111100
	.db %11100000,%00011100,%00000111,%00001000
	.db %11110000,%00001111,%00001111,%11110000
	.db %11100000,%00010010,%00000111,%10001000
	.db %11000000,%00100001,%00000011,%00000100
	.db %11000000,%00100001,%00000011,%00000100
	.db %11000000,%00110000,%00000111,%10001000
	.db %11000000,%00111001,%00001111,%11110000
	.db %11000000,%00101111,%00000111,%00001000
	.db %11000000,%00100000,%00000111,%00001000
	.db %11100000,%00010000,%00001111,%01010000
	.db %11100000,%00010000,%00001111,%01010000
	.db %11110000,%00001111,%00011111,%11100000
	.db %11110000,%00001000,%00001111,%01010000
	.db %11110000,%00001000,%00000111,%00101000
	.db %11110000,%00001111,%00000111,%11111000

 

La routine qui permet de copier un sprite dans le GRAPH_MEM est gba_drawsprite. Il suffit de stocker dans hl l'adresse du sprite, dans d sa largeur en octets, dans e sa hauteur en pixels, dans b et c ses coordonnées (x,y) à l'écran ( le point (0,0) se situe en haut à gauche):

	; ...
	ld hl,Mario_sprite
	ld de,$021c			; d=2, e=28
	ld bc,$1024			; b=16, c=36
	call gba_drawsprite
	; ...

(Voir Exemple3.asm)


Gba_drawsprite s'occupe aussi de couper les images si elles sortent du cadre de l'écran, pour ne pas corrompre la mémoire. Vous n'aurez donc pas à vous soucier des coordonnées d'un sprite avant de l'afficher, ce qui simplifiera beaucoup vos programmes. Voici un petit exemple qui illustre ce principe:

(Voir Exemple4.asm)

 

2) Organiser vos sprites dans votre programme

Quand il faut gérer une dizaine d'ennemis en même temps dans un jeu, il est plus facile de regrouper toutes les données des ennemis présents à l'écran dans un tableau, où chaque ligne (liste) représente un sprite, et chaque colonne une caractéristique de ce sprite. Par exemple on veut écrit une table où chaque ennemi est caractérisé par ses coordonnées (2 octets), l'adresse de son image (2 octets), ses points de vie (1 octets), et l'adresse de la routine qui lui sert d'AI (2 octets):

Ennemi_table:
Ennemi_1:
.db 0,0,0,0,0,0,0	; coord y, coord x, adresse img, PV, adresse AI

Ennemi_2:
.db 0,0,0,0,0,0,0	; coord y, coord x, adresse img, PV, adresse AI

Ennemi_3:
.db 0,0,0,0,0,0,0	; ...

Ennemi_4:
.db 0,0,0,0,0,0,0
Ennemi_5:			; 5 sprites max
.db 0,0,0,0,0,0,0


Vous pouvez ainsi écrire une routine qui affiche les ennemis à l'écran::

Afficher_ennemis:
	ld hl,Ennemi_table
	ld b,5
GE_boucle:
	push bc
	ld c,(hl)			; coord y
	inc hl
	ld b,(hl)			; coord x
	inc hl
	ld e,(hl)			; img
	inc hl
	ld d,(hl)

	inc hl
	push hl

	ex de,hl
	ld e,(hl)			; sprite dim
	inc hl
	ld d,(hl)			; sprite dim
	inc hl

	call gba_drawsprite
	pop hl
	inc hl
	inc hl
	pop bc
	djnz GE_boucle
	ret


Et voici comment les images des sprites sont stockées à la fin du programme:

Ennemi_Img:
 .db dimy,dimx
 .db %...,%...		; image
 .db %...,%... 

où dimy et dimx sont la hauteur et la largeur de l'image du sprite.

 

Retour à la programmation avec GBA : l'adresse de votre table sera stockée dans la variable 16 bits sprite_table. Le nombre d'informations (octets) nécessaires pour décrire un sprite sera stocké dans la variable 16 bits l_sprite. Enfin le nombre de sprites maximum sera stocké dans la variable 8 bits sprite_max. La longueur de votre table sera donc de l_sprite*sprite_max. Dans l'exemple précédent, on aura donc:

	ld hl,Ennemi_table
	ld (sprite_table),hl
	ld bc,7
	ld (l_sprite),bc
	ld a,5
	ld (sprite_max),a

Vous devez cependant respecter certaines contraintes : les 3 premiers octets de chaque liste de la table ne doivent pas être modifiés, vous apprendrez à les utiliser dans le paragraphe suivant. Les 2 octets suivants de chaque liste sont les coordonnées y et x du sprite à l'écran. Vous pouvez utiliser les octets suivants comme vous le voulez.

3) Inclure des sprites dans vos cartes

Dans les jeux-vidéo, il est courant de vouloir placer les ennemis à des endroits bien précis de la carte : pendant que vous progressez dans un niveau, des ennemis apparaissent (c'est-à-dire sont chargés dans la table des sprites présents à l'écran) ; si vous les tuez puis quittez le niveau et revenez au même endroit, ils reviennent. La question est : sous quelle forme et à quel endroit va-t-on stocker les informations nécessaires pour que ces sprites soient chargés au bon moment?

Tout d'abord, chaque sprite est caractérisé par son type : par exemple dans Super Mario il y a les tortues, les tortues volantes, les plantes carnivores, les boulets de canon... Deux sprites de type identique ont le même comportement (IA), la même image, le même nombre de points de vie initial... Il s'agit en fait du même sprite! Il n'est pas rare de voir sur un écran 2 ou 3 fois le même ennemi, il s'agit tout simplement de sprites de même type. A chaque type correspond un numéro (attribué arbitrairement), et c'est ce numéro qui va être utilisé pour inclure des personnages dans les cartes.

Pour que les sprites soient chargés pendant qu'on avance dans un niveau, j'ai tout d'abord pensé qu'il fallait créer une table pour chaque carte contenant les types de sprites et leur coordonnées dans celle-ci, mais c'est une méthode que l'on retouve surtout pour les jeux qui n'utilisent que les scrolls horizontaux (comme Super Mario ti82 de Sam Heald). Elle est peu pratique pour les jeux avec scrolls multidirectionnels. J'ai finalement opté pour l'inclusion des sprites directement dans les matrices des cartes. Il faut bien sûr indiquer à GBA quels numéros correspondent à des tiles et quels numéros correspondent à des sprites! C'est pourquoi j'ai défini la variable 8bits n_sprite : tous les numéros compris entre 0 et n_sprite-1 sont des tiles et seront affichés normalement, tandis que tous les numéros compris entre n_sprite et 255 sont des types de sprites (codés sur 8bits).

Mais un autre problème se pose : si un ennemi est chargé dans la table des sprites, il ne doit l'être qu'une seule fois! S'il reste à l'écran, et que vous revenez en arrière puis en avant, il est hors de question de l'afficher une autre fois alors que l'autre n'a pas disparu! Il faut donc faire la distinction entre les sprites qui doivent être chargés au moment où la carte est affichée (les ennemis "vivants"), et ceux qui ne doivent pas l'être (les ennemis déjà présents à l'écran et ceux qui sont "morts")...

J'ai choisi cette solution : les sprites doivent tous avoir un type qui est un nombre pair. Les nombres impairs sont réservés aux sprites qui ne devront plus être chargés dans la table de sprites. Par exemple, si une tortue ninja est caractérisée par le type 6, une tortue ninja "morte" est caractérisée par le type 6+1=7.

 

4) Chargement d'un sprite dans la table

Voyons maintenant comment GBA gère concrètement le chargement des sprites dans la table.

Alors que vous effectuez un défilement de la carte, le numéro des tiles qui vont être affiché aux bords de l'écran sont examinés; appelons N le numéro d'un tile, et ADR son adresse dans la mémoire (adresse du genre (MAP_ADDR)+...). Si N < n-sprite, il s'agit d'un tile habituel (cf paragraphe précédent). Si N >= n-sprite, il s'agit d'un sprite, et les étapes suivantes sont exécutées:

Si N est impair, ce sprite est ignoré. Le programme retourne à l'exécution habituelle. Si N est pair, le programme modifie la carte:

	ld hl,ADR
	inc (hl)

Il incrémente le numéro du sprite pour ne pas l'afficher plusieurs fois. Puis il va chercher un emplacement libre dans la table des sprites, avec la routine gba_findsprite. S'il n'y a pas de place dans la table, le sprite est ignoré et le programme continue normalement. Vous vous souvenez des 3 octets qui ne devaient pas être changés dans chaque liste? (cf paragraphe 2) Dans les deux premiers sera stockée l'adresse ADR, appelée identité du sprite, et dans le troisième sera stocké le nombre N-n_sprite, qui correspond à son type (c'est un nombre pair, rappelons-le!). Enfin, le programme stocke dans les deux octets suivants les coordonnées y et x du sprite.

Remarque importante : La routine gba_findsprite est basée sur un principe simple : l'identité d'un sprite n'est jamais nulle, car il s'agit d'une adresse qui pointe à l'intérieur de votre programme (donc différente de $0000). Pour savoir si une liste est libre ou non, il suffit de lire les 2 premiers octets : s'ils sont nuls, la place est libre, sinon il y a déjà un sprite. Peu importe la valeur des octets suivants. Pour supprimer un sprite de la liste, il suffit donc de mettre zéro à l'identité.

La variable news_prite : Dans cette variable, vous stockerez l'adresse d'une routine qui sera executée juste après ce qui précède. Au moment où cette routine est appelée, le registre de pointe sur le 6e octet de la liste, a est égal au type du sprite. Cela est très utile pour initialiser les données diverses concernant ce sprite. Si vous ne voulez pas utilier cette routine, vous laisserez ce paramètre à sa valeur initiale, zéro.

Dans le programme suivant, on écrit un routine qui initialise les points de vies d'un ennemi en fonction de son type:

	ld hl,Nouveau_sprite
	ld (new_sprite),hl

	;...


Nouveau_sprite:
	ld hl,Table_ptsdevie
	srl a					; car a est un multiple de 2
	ld b,0
	ld c,a
	add hl,bc
	ld a,(hl)
	ld (de),a
	ret


Table_sprite:
.db 0,0,0,0,0,0				; identité (2 octets), type, y et x coords, points de vie
.db 0,0,0,0,0,0
.db 0,0,0,0,0,0


Table_ptsdevie:
.db 6,6,12,50,82,125

Bien sûr, lorsque vous effectuez un défilement, les coordonnées des sprites sont automatiquement changés.

Enfin, voici un petit programme synthétique qui regroupe ce qu'on a vu:

(Voir Exemple5.asm)

On remarque plusieurs bugs dans cet exemple:

- tout d'abord le programme ne marche qu'une seule fois : si on le relance, les sprites ne s'affichent plus! C'est tout à fait normal, car la carte a été modifiée la première fois et tous les sprites sont devenus des sprites "morts" (avec un type impair). Il suffit pour résoudre le bug d'appeler, juste avant gba_initmap, la routine gba_initsprite, qui "fait revivre" tous les sprites de la carte et initialise aussi la table des sprites.

- parmi les 6 ennemis présents dans la carte (représentés par les numéros 240), seuls 5 ennemis apparaissent. Eh oui! La table ne peut contenir que 5 sprites au maximum, et donc les sprites suivants seront ignorés. Pour résoudre ce bug, il faut soit augmenter la capacité de la table, soit effacer les ennemis de la table lorsqu'ils disparaissent (le sujet de notre prochain paragraphe). Voici une version corrigée de l'exemple 5 :

(Voir Exemple6.asm)

5) Supprimer un sprite

Vous l'avez compris, à moins de faire une table des sprites de 500 octets de long, il faut effacer directement ces sprites de la table lorsqu'ils sortent de l'écran, votre jeu n'en sera que plus rapide. Très simple:

 					; hl pointe sur le premier octet de l'identité d'un sprite
	ld (hl),0
	inc hl
	ld (hl),0		; supprime le sprite de la liste


D'accord, me direz-vous, mais si on efface un sprite parce qu'il sort de l'écran, il devrait en principe être chargé une nouvelle fois dans la table si on revient, pourtant ce n'est pas le cas... Vous avez tout à fait raison ; il faut dans ce cas restaurer la valeur initiale de l'octet qui a été modifié au chargement du sprite:


					; hl pointe sur le premier octet de l'identité d'un sprite
	push hl

	ld e,(hl)
	inc hl
	ld d,(hl)
	inc hl
	ex de,hl		; hl=identité
	dec (hl)		; restaure la valeur initiale de l'octet qui a été modifié
	pop hl
	ld (hl),0
	inc hl
	ld (hl),0		; supprime le sprite de la liste
	


IV) Compléments

1) Quelques routines et variables supplémentaires

gba_collision: Cette routine est très utile, surtout dans les jeux de plate-forme ; vous stockez dans (b,c) les coordonnées, positives ou négatives, d'un point à l'écran (ou en dehors), et cette routine vous renvoie dans a le numéro du tile qui contient ce point et dans hl l'adresse de ce tile.

gba_hl_plus_dexbc: Une routine très utilisée en interne par GBA. Le résultat donne hl+de*bc -> hl, avec de positif ou négatif, et bc strictement positif.

gba_drawinverted: Cette routine fonctionne exactement comme gba_drawsprite, à une différence près... L'image est inversée comme sur un film photo : les pixels blancs et noirs sont interchangés. Ce tableau montre les différentes possibilités :

bit de l'image bit du masque gba_drawsprite gba_drawinverted
0 0 blanc noir
1 0 noir blanc
1 1 noir noir
0 1 transparent transparent

gba_findsprite: Cette routine va chercher dans la table des sprites un emplacement libre pour y stocker les données d'un nouveau sprite. S'il y a de la place, hl pointera sur le premier octet de la liste (qui sert pour l'identité). Sinon le zéro flag vaudra not zero.

tile_zero: Peut-être avez-vous déjà été confronté au problème suivant : après avoir dessiné un magnifique background, vous constatez que vous ne pouvez pas inclure de sprite dans votre carte sans laisser un gros trou blanc, là où un élément de décor a été remplacé par le sprite... Pas de panique, voici ce que je vous propose : vous indiquerez à GBA quel tile doit être affiché à la place d'un trou blanc laissé par la présence d'un sprite. tile_zero est en fait le numéro (8bits) du tile qui est considéré comme "papier peint", il est initialement à zero (d'où son nom).

n_tiles2: Parfois dans certains jeux comme Lemmings ou Worms, on a besoin de faire des modifications imortantes dans la carte, comme casser des blocs, creuser ... Le problème est qu'il faut quand même conserver en mémoire la carte originale, sous peine de ne pouvoir jouer qu'une seule fois avant de télécharger le jeu à nouveau. Alors, à moins de copier toute la carte à un endroit sûr de la ram (ce qui peut se révéler problématique avec des cartes de 5000 octets), il va falloir trouver un moyen de modifier directement la carte de manière réversible.

J'ai choisi cette solution : supposons que le jeu utilise à peu près 90 tiles différents, et une vingtaine d'ennemis. Si on veut effacer un tile de la carte, il suffira par exemple de rajouter 100 au numéro du tile et faire en sorte que tous les tiles dont le numéro est supérieur à 100 soit affichés en blanc. Pour faire "réapparaître" les tiles, il suffira de soustraire 100 à tous ceux dont le numéro et supérieur à 100. C'est là qu'arrive le paramètre 8bits n_tile2 : tous les tiles dont le numéro est inférieur à n_tiles2 sont traités normalement, ceux ayant un numéro compris entre n_tiles2 et n_sprite-1 seront affichés comme le tile tile_zero.

Mais l'utilité de cette variable ne s'arrête pas là! Vous pouvez vous servir de tiles ayant un numéro supérieur à n_tiles2 comme de marqueurs : par exemple, on peut facilement faire exécuter une routine lorsque votre personnage arrive sur l'un de ces tiles (en appelant la simple routine gba_collision), ce qui est très pratique pour gérer les changements de carte, les évènements inattendus...

 

2) Récapitultif de toutes les fonctions de GBA

Variables:

nom de la variable
taille
description
Voir paragraphe...
Valeur initiale
screen_width
8bits
largeur de la fenêtre en octet
II) 4)
12
screen_height
8bits
hauteur de la fenêtre en octet
II) 4)
10
zero_point
16bits
adresse du point en haut à gauche de la fenêtre, moins GRAPH_MEM
II) 4)
0
new_sprite
16bits
adresse de la routine exécutée lorsqu'un sprite est chargé dans la table des sprites ( de pointe sur le 6e octet d'une liste de la table, a est égal au type du sprite qui vient d'être créé)
III) 4)
$0000
tile_zero
8bits
numéro du tile à afficher "par défaut", à l'endroit où un sprite est créé, ou pour les tiles dont le numéro est supérieur à n_tiles2
IV) 1)
0
n_tiles2
8bits
tous les tiles dont le numéro est supérieur ou égal à n_tiles2 seront affichés comme ayant un numéro égal à tile_zero
IV) 1)
0
n_sprite
8bits
définit la différence, dans la matrice de la carte, entre un tile (numéro strictement inférieur à n_sprite) et un sprite (numéro supérieur ou égal à n_sprite)
III) 3)
0
sprite_table
16bits
adresse de la table des sprites
III) 2)
$0000
sprite_max
8bits
nombre de sprites que peut gérer la table en même temps
III) 2)
0
l_sprite
16bits
nombre d'informations (en octet) qui caractérisent un sprite dans la table
III) 2)
0
tiles_addr
16bits
adresse de la liste des tiles
II) 2)
0
map_addr
16bits
adresse de la matrice de la carte
II) 2
$0000
map_width
16bits
nombre de colonnes de cette matrice
II) 2)
$0000
map_height
16bits
nombre de lignes de cette matrice
II) 2)
0
scroll_y
16bits
nombre de tiles entiers dépassés par la fenêtre à gauche
II) 2)
0
scroll_x
16bits
nombre de tiles entiers dépassés par la fenêtre en haut
II) 2)
0

Routines:

nom de la routine description Voir paragraphe... entrée sortie
gba_initmap affiche la carte dans l'APD_BUF II) 2) - -
gba_hl_plus_dexbc hl+de*bc -> hl IV) 1) bc non nul positif, de positif ou négatif -
gba_restoremap copie la carte de l'APD_BUF dans le GRAPH_MEM II) 2) - -
gba_scroll_right effectue un défilement vers la droite II) 3) - -
gba_scroll_left effectue un défilement vers la gauche II) 3) - -
gba_scroll_up effectue un défilement vers le haut II) 3) - -
gba_scroll_down effectue un défilement vers le bas II) 3) - -
gba_collision renvoie le numéro et l'adresse du tile qui contient le point situé à (b,c) IV) 1) (b,c) coordonnées d'un point, peuvent être négatives ou en dehors de l'écran a est égal au numéro du tile, hl contient l'adresse de ce tile
gba_findsprite cherche un emplacement vide dans la table des sprites IV) 4) -

-s'il y a de la place: hl pointe sur le 1er octet d'une liste vie

- sinon le zéro flag vaut not zero

gba_initsprite restaure les modifications dûes au chargement de sprites dans la table IV) 4) - -
gba_drawsprite affiche un sprite avec masque III) 1) (b,c) coordonnées du sprite, (d,e) largeur en octet et hauteur en pixels du sprite, hl l'adresse de l'image -
gba_drawinverted affiche un sprite avec masque, en négatif IV) 1) idem -