1. Introduction▲
Nous savons que les sprites se voient décrits en mémoire au sein de la mémoire OAM. Cette portion de mémoire accueille 128 entités décrites par la structure t_OAMEntry décrite en détail dans le listing numéro un. En modifiant judicieusement les bits des différents attributs d'une entité OAM, nous parviendrons à manipuler nos sprites. Toutefois, la mémoire OAM ne peut se voir modifiée à tout moment. Les modifications doivent intervenir après une période de synchronisation verticale de l'écran seulement. C'est pourquoi notre code source emploie un tampon mémoire que nous recopierons dans la mémoire OAM après une période de v-blank (synchronisation verticale).
Voici la définition de notre tampon :
#define MAX_SPRITES 128
OAMEntry OAMBuffer[MAX_SPRITES];
Ainsi nous devrons rédiger la boucle principale du programme de cette manière :
while
(
1
)
{
waitForVSync
(
);
copyOAMBuffer
(
);
}
La copie du tampon dans la mémoire OAM pourrait se réaliser très simplement en utilisant une boucle itérative recopiant chaque entité du tampon dans la mémoire OAM. Toutefois, ce procédé se révèle relativement lent et peut se voir avantageusement substitué à l'utilisation de la copie mémoire directe (DMA). Par une extraordinaire coincïdence, nous avons récemment étudié le fonctionnement du DMA.
Finalement, nous pourrons écrire la fonction copyOAMBuffer() :
u16*
OAM =
(
u16*
) 0x7000000
;
void
copyOAMBuffer
(
)
{
REG_DMA3SAD =
(
u32) OAMBuffer;
REG_DMA3DAD =
(
u32) OAM;
REG_DMA3CNT =
MAX_SPRITES *
4
|
DMA_16NOW;
}
La première ligne correspond à la création d'un pointeur sur la mémoire OAM. La copie DMA demande une copie de 4 * 128 * 2 octets (l'unité est de 16 bits ici) car chaque entité OAM contient quatre segments de données sur 16 bits. Nous pouvons maintenant procéder à la création de sprites dans notre programme.
2. Créer un sprite▲
Pour simplifier la gestion des sprites dans le programme principal, nous allons nous employer à créer quelques utilitaires supplémentaires, bien que ceux-ci ne soient pas indispensables. Premièrement, une fonction d'initialisation des sprites se révèle très utile. Le but de cette fonction consiste simplement à positionner l'ensemble des entités OAM à des coordonnées situées en dehors de l'écran :
for
(
int
i =
0
; i <
MAX_SPRITES; i++
)
{
OAMBuffer[i].attribute0 =
160
;
OAMBuffer[i].attribute1 =
240
;
OAMBuffer[i].attribute2 =
0
;
}
Souvenez-vous que les huit premiers bits du premier attribut désignent la coordonée y du sprite, et les neuf premiers du second attribut désignent la coordonnée x.
Ensuite, nous chargeons les données du sprite en mémoire. Nous allons dans un premier temps nous intéresser uniquement aux sprites en 256 couleurs. Les données du sprites se trouvent au sein du fichier xwing.h, généré par l'outil pcx2sprite. Ce fichier recèle deux tableaux : celui des tiles du sprites et celui de la palette de couleurs. La première étape consiste à charger en mémoire la palette de couleurs :
u16*
spritesPalette =
(
u16*
) 0x5000200
;
for
(
int
i =
0
; i <
256
; i++
)
spritesPalette[i] =
xwingPalette[i];
Nous avons ici conservé une boucle simple par souci de clareté, néanmoins rien n'interdit l'emploi de la copie DMA. La zone mémoire relative à la palette des sprites est différente de celle consacrée aux modes graphiques bitmap (tel le mode 4). Nous vérifierons cela en affichant ultérieurement une image en mode 4 derrière notre sprite. Notez également que la première couleur de la palette définit la couleur transparente. Ceci permet de superposer sprites et décors sans problèmes. La seconde étape concerne la lecture des tiles composant le sprite.
Ces données doivent être placées dans la mémoire OAM des objets qui se trouve à l'adresse 0x601000.
REG_DMA3SAD =
(
u32) xwingData;
REG_DMA3DAD =
(
u32) (&
OAMData[512
*
16
]);
REG_DMA3CNT =
256
*
8
|
DMA_16NOW;
Ces quelques lignes requièrent plusieurs explications. Premièrement, l'indice de la mémoire OAMData utilisé n'est pas zéro, mais 512*16. L'explication tient dans le fait que notre programme emploie le mode 4 avec une image en arrière plan. De ce fait, ces données se trouvent déjà exploitées. Par la suite, nous copions 256 * 8 segments de 16 bits dans la mémoire OAM des objets. En effet, nous utilisons un sprite de 64x64 pixels. Celui-ci se représente en mémoire sous la forme de blocs (les tiles) de 8x8 pixels. Un sprite correspond donc à 8x8 blocs de 8x8 pixels. Ainsi, une ligne de tiles occupe 8x8x8 = 512 octets. Or nous avons 8 lignes de tiles, donc 512 * 8 octets à copier. Puisque la copie DMA s'effectue par segments de 16 bits, nous devons copier 256 * 8 segments de 16 bits. L'affichage d'un sprite demande dorénavant l'initialisation de l'une des 128 entités OAM.
3. La structure sprite▲
Le fichier sprites.h déclare une structure supplémentaire nommée t_Sprite (voir listing numéro deux). Dans un premier temps, nous allons nous intéresser uniquement aux champs spriteID, oam, x et y. La valeur du spriteID définit l'indice du sprite dans le tampon OAM. L'adresse de l'entité OAM liée au sprite se voit représentée par le champ oam. Enfin, les données x et y définissent seulement la position du sprite à l'écran. Pour remplir ces champs, nous faisons appel à la fonction createSprite() qui reçoit en paramètre le spriteID et le rotationID (sur lequel nous reviendrons par la suite). Reportez-vous au listing numéro trois pour découvrir la création d'un sprite.
Le remplissage des champs de l'entité OAM du sprite constitue la dernière étape du procédé d'affiche d'un sprite :
xwing =
createSprite
(
0
, 0
);
xwing->
x =
0
;
xwing->
y =
0
;
xwing->
oam->
attribute0 =
COLOR_256 |
SQUARE |
xwing->
y;
xwing->
oam->
attribute1 =
SIZE_64 |
xwing->
x;
xwing->
oam->
attribute2 =
xwing->
spriteID +
512
;
Dans le premier attribut ces instructions précisent que le sprite emploie une palette de 256 couleurs. La propriété SQUARE définit la taille du rectangle de clipping créé par la console. Un type SQUARE définit une zone de 64x64 pixels (voir le deuxième attribut). Vous pouvez également faire appel aux types TALL (32x64 pixels) et WIDE (64x32 pixels). Le second attribut précise la taille du sprite (ici 64x64 pixels, mais nous aurions pu utilise SIZE_8, 16 ou 32). Pour terminer, le troisième attribut indique l'adresse du premier tile du sprite. Le saut de 512 est également du au mode 4. En mode 2 ou 3, cela n'aurait pas lieu d'être.
Sans la précision du mode graphique, notre programme ne pourrait fonctionner.
REG_DISPCNT =
MODE_4 |
BG2_ENABLE |
OBJ_ENABLE |
OBJ_MAP_1D;
Cette ligne de code met en place le mode graphique 4 et active les couches d'affichage. Les couches activées sont celle des fonds d'écran de priorité 2 et des objets (donc des sprites). Sans la mention OBJ_ENABLE, vos sprites ne pourraient être affichés. La propriété OBJ_MAP_1D décrit la méthode de présentation des données en mémoire du sprite. Ces données peuvent prendre la forme d'un tableau (OBJ_MAP_2D) ou d'une pile. Le mapping 2D ne semble pas posséder d'intérêt flagrant, aussi conserverons-nous constamment le mapping 1D (cette méthode de mapping nous permet de copier toutes nos données de sprite dans la mémoire OAMData par l'intermédiaire du DMA).
4. Déplacer un sprite▲
Un jeu dont les sprites restent figés ne possède que très peu d'intérêt. Nous devons donc faire se mouvoir nos sprites. Comme la structure t_Sprite possède les champs nécessaires à une telle opération, une simple fonction suffira.
void
moveSprite
(
pSprite lpSprite, int
x, int
y)
{
lpSprite->
oam->
attribute0 &=
0xFF00
;
lpSprite->
oam->
attribute0 |=
y;
lpSprite->
oam->
attribute1 &=
0xFE00
;
lpSprite->
oam->
attribute1 |=
x;
}
Par souçi de souplesse, la fonction prend en paramètre les nouvelles coordonnées x et y du sprite bien que le paramètre lpSprite soit supposé receler de telles informations. Avant de mettre à jour les coordonnées de l'entité OAM liée au sprite, la fonction moveSprite() "efface" les bits correspondants auxdites coordonnées. Un logiciel ne prenant pas cette précaution obtiendra des résultats erratiques et particulièrement difficiles à appréhender en terme de débogage. En suivant le même principe que le mois précédent, notre programme implémente une fonction getInput() destinée à offrir une once d'interactivité à l'utilisateur.
5. Rotations▲
La GameBoy Advance octroie l'opportunité de réaliser des rotations sur des sprites. Avant de commencer, nous devons définir une nouvelle structure, présentée dans le listing numéro quatre. Jusqu'à présent, le quatrième attribut des entités OAM ne possédait aucune utilité. En vérité, cet attribut est destiné à recevoir des informations sur la rotation des sprites. Pour provoquer la rotation d'un sprite, le programmeur doit définir la position des quatre coins de la zone de clipping du sprite. Une rotation nécessite donc l'emploi de quatre attributs ! La mémoire OAM contient 128 entités contenant chacune un attribut inemployé.
Nous avons donc à notre disposition 128 / 4 = 32 rotations possibles. Une structure t_RotData recouvre quatre structures t_OAMEntry :
pRotData rotationData =
(
pRotData) OAMBuffer;
Nous pourrons ainsi numéroter nos rotations de 0 à 31 et accéder à leurs quatre valeurs corner1 à corner4 plus facilement qu'en jouant sur le tampon OAMBuffer. Les champs intitulés "filler" dans la structure t_RotData permettent de "sauter" les trois premiers attributs des entités OAM auxquels nous ne devons pas toucher. Rappelez-vous que notre structure t_Sprite laissait le loisir de spéficier un identificateur de rotation. Cet identificateur sera utilisé par nos fonctions pour savoir dans quelle structure de rotation aller chercher les informations requises.
void
rotateSprite
(
pSprite lpSprite, int
angle, s32 xscale, s32 yscale)
{
rotationData[lpSprite->
rotationID].corner1 =
(
u16) (((
xscale) *
COS[angle]) >>
8
);
rotationData[lpSprite->
rotationID].corner2 =
(
u16) (((
yscale) *
SIN[angle]) >>
8
);
rotationData[lpSprite->
rotationID].corner3 =
(
u16) (((
xscale) *
-
SIN[angle]) >>
8
);
rotationData[lpSprite->
rotationID].corner4 =
(
u16) (((
yscale) *
COS[angle]) >>
8
);
}
Cette fonction permet la rotation et la mise à l'échelle d'un sprite. Les deux paramètres xscale et yscale, aussi représentés dans t_Sprite, donnent la chance d'agrandir ou de rapetisser le sprite. Cette caractéristique se trouve régulièrement employés dans les jeux pour donner l'impression qu'un sprite provient du fond de l'image. Les calculs effectués ici sont de simples calculs trigonométriques de rotation. Le décalage à droite de 8 bits désigne une astuce de programmation pour manipuler des nombres à virgule fixe. Les calculs des cosinus et de sinus demandent beaucoup de ressources au processeur.
C'est pourquoi nous nous référons à des tables de cosinus et de sinus précalculées. Le calcul de ces tables se réalise ainsi :
for
(
int
i =
0
; i <
360
; i++
)
{
COS[i] =
(
s32) (
cos
(
RADIAN
(
i)) *
256
);
SIN[i] =
(
s32) (
sin
(
RADIAN
(
i)) *
256
);
}
La multiplication par 256 concerne toujours les nombres à virgule fixe. Pourtant, le calcul des tables nécessite encore trop de temps. Si vous calculez les tables ainsi, une à plusieurs secondes pourront s'écouler avant l'affichage du sprite. Une solution bien plus efficace consiste à créer des tableaux constants contenant toutes les valeurs. Ces tableaux se voient inclus directement dans le code source, et généré par un programme externe.
const
signed
long
SIN[] =
{
0
.0
, 4
.4678
, 8
.9342
, 13
.398
. }
Pour ne pas pénaliser les temps de compilation, les tables trigonométriques, tout comme les images, sont compilées séparémment et désignées par des variables "extern" :
extern
s32 COS[];
extern
s32 SIN[];
La rotation d'un sprite demande également de modifier les données de l'entité OAM correspondante. Premièrement, nous devons activer le drapeau de rotation, sinon seuls les renversements horizontal et vertical seront permis.
xwing->
oam->
attribute0 =
COLOR_256 |
/* SIZE_DOUBLE | */
ROTATION_FLAG |
SQUARE |
xwing->
y;
Le drapeau SIZE_DOUBLE donne la possibilité de multiplier par deux la surface de clipping du sprite. Lors d'une rotation, les bords du sprites pourront se voir éliminés par phénomène de clipping (cela n'est pas vrai pour notre exemple). Pour y remédier, il vous suffira d'activer ce drapeau. Le second attribut OAM doit en outre désigner la structure de rotation liée au sprite :
xwing->
oam->
attribute1 =
SIZE_64 |
ROTDATA
(
xwing->
rotationID) |
xwing->
x;
La macro ROTDATA() se contente de décaler le nombre passé en paramètre de neuf bits vers la gauche. La fonction getInput() définit l'utilisation de touches pour appliquer une rotation progressive au sprite. Afin de mieux comprendre le fonctionnement des sprites, vous pourrez essayer de lier deux touches de la machine à l'agrandissement et à la réduction du sprite. La prochaine étape de notre dossier sur la programmation GBA concernera l'affichage de décors composés de tiles.
6. Listing 1▲
typedef
struct
t_OAMEntry
{
u16 attribute0;
u16 attribute1;
u16 attribute2;
u16 attribute3;
}
OAMEntry, *
pOAMEntry;
7. Listing 2▲
typedef
struct
t_Sprite
{
u16 spriteID;
u16 rotationID;
pOAMEntry oam;
int
x, y, angle;
s32 xscale, yscale;
}
Sprite, *
pSprite;
8. Listing 3▲
pSprite createSprite
(
u16 spriteID, u16 rotationID)
{
pSprite sprite =
new Sprite;
sprite->
spriteID =
spriteID;
sprite->
rotationID =
rotationID;
sprite->
x =
240
;
sprite->
y =
160
;
sprite->
angle =
0
;
sprite->
xscale =
(
1
<<
8
);
sprite->
yscale =
(
1
<<
8
);
sprite->
oam =
&
OAMBuffer[spriteID];
return
sprite;
}
9. Listing 4▲
typedef
struct
t_RotData
{
u16 filler1[3
];
u16 corner1;
u16 filler2[3
];
u16 corner2;
u16 filler3[3
];
u16 corner3;
u16 filler4[3
];
u16 corner4;
}
RotData, *
pRotData;