1. Les touches▲
La merveilleuse console que représente la GameBoy Advance propose un pavé multi-directionnel, deux touches de fonction et quatre touches de "jeu". Nous avons ainsi à notre disposition un ensemble de 10 touches, le pavé directionnel se composant lui-même de quatre touches distinctes. Contrairement à de nombreux autres langages, le développeur ne récupère pas les événements générés par les appuis sur les touches, mais se doit de faire l'effort d'aller chercher les informations lui-même. En Java ou Visual Basic par exemple, un événement se voit émis lors de l'appui sur une touche ou de son relâchement. Un événement de ce type encapsule notamment le type de la touche à l'origine de l'interruption.
Lors de notre initiation aux modes graphiques 3 et 4, nous avons découvert l'emploi de nombreux registres afin de réaliser des opérations diverses : initialisation de l'écran, copie mémoire directe... Les touches de la GameBoy Advance se révèlent gérées de la même manière, soit par l'entremise d'un registre.
2. Le registre des touches▲
Le registre recueillant les informations à propos des touches pressées par le joueur se situe à l'adresse mémoire 0x04000130. Nous définissons donc notre nouveau registre, ainsi que nous l'avons déjà fait pour les registres vidéo et palette de couleur :
volatile
u32*
KEYS =
(
volatile
u32*
) 0x04000130
;
Notre registre, intitulé KEYS, s'avère posséder une capacité de 32 bits. La machine ne possède en tout et pour tout que dix touches, aussi 22 bits semblent inutilisés. En réalité, les 32 bits de notre registre se décomposent en deux segments distincts de 16 bits. Le premier, à l'adresse relative 0x130, donc l'adresse absolue 0x04000130, correspond au status des touches. Les 10 touches du matériel correspondent dans ce segment à 10 bits de statut. Lorsqu'un bit se trouve positionné à 1, la touche n'est pas enfoncée. Lorsque le bit se trouve 0, il y a appui de la part du joueur sur la touche considérée. La mention volatile qui préfixe le type de données du registre précise au compilateur que ce registre peut se voir modifié par une entité extérieure. Ainsi aucune optimisation spéciale ne sera réalisée sur les lignes de code utilisant ce registre. Ceci s'avère indispensable pour la fonction d'attente que nous verrons un peu plus loin.
Qu'en est-il alors du second segment de 16 bits ? Ce second segment correspond en vérité au contrôle d'interruption des touches. Nous ne nécessiterons pas ce registre avant d'avoir étudié plus avant les interruptions sur GameBoy Advance. En conservant uniquement le registre des statuts, le développeur se voit octroyé l'opportunité de teste très rapidement l'appui sur une touche :
// appui sur START
if
(!(*
KEYS &
(
1
<<
3
)))
startPressed
(
);
3. L'emploi du registre▲
Etudions le contenu du premier segement de 16 bits lors de l'exécution de ces instructions, en imaginant que la touche Start et la touche A sont pressées en même temps. L'encadré numéro démontre la validité de notre méthode. Le résultat de l'opération ET binaire produit une suite de zéros. L'application du NON logique entraîne donc la vérité du test, la touche Start est bien enfoncée. En observant le schéma numéro un, essayez de réitérer le calcul en vérifiant successivement les appuis sur les touches A et Select. De ce schéma, nous pouvons également déduire les définitions nécessaires pour employer le registre de touches de manière efficace dans notre code (voir listing numéro un).
Nanti de ces précieuses données, nous pouvons dors et déjà comprendre le fonctionnement de la condition d'arrêt de la boucle principale du programme :
while
(*
KEYS &
KEY_START)
{
displayPlasma
(
g, x, y);
waitForVSync
(
);
getInput
(
g, x, y);
}
Cette boucle se verra exécutée tant que la touche Start n'aura pas été enfoncée. La mise en place des définitions précédentes octroie également la chance de réaliser une fonction dédiée à la gestion des touches. Nous observons ici la différence précisée précédemment en regard d'autres langages. La fonction getInput() de notre programme va directement lire la mémoire du matériel afin de déterminer le status des touches. En voici un court extrait :
if
(!(*
KEYS &
KEY_DOWN))
{
y++
;
if
(
y >
g->
height)
y =
0
;
}
4. Touches et nombres aléatoires▲
Une fonction particulière destinée à attendre l'appui sur une touche peut se révéler particulièrement pertinente au sein d'un logiciel. Souvenez-vous que lors de l'exécution du programme, l'utilisateur doit au préalable presser la touche Start pour générer un terrain. Selon le temps mis par l'utilisateur pour presser la touche, le terrain différera d'un autre. La fonction dont nous discutons à présent possède un double emploi : attente de l'utilisateur, en ce qui concerne l'écran titre par exemple, et modification de la graine de génération des nombres aléatoires.
Le code d'attente se révèle extrêmement simple :
void
waitKey
(
int
key)
{
while
(
1
)
if
(!(*
KEYS &
key)) break
;
}
Un simple appel tel que waitKey(KEY_START) laissera votre programme en attente d'un appui sur Start. Toutefois, nous pouvons compléter cette fonction afin de lui ajouter la capacité de réinitialiser la graine de génération de nombres aléatoires. Pour cela, l'étude des timers demeure nécessaire.
5. Les timers▲
Les timers, au nombre de quatre au sein de la GBA, comptent le temps en se basant sur l'horloge système. Chaque timer comprend 16 bits pour les données ainsi que 16 bits pour le contrôle. Le schéma numéro deux explique le contenu des 16 bits des registres de contrôle des timers. Chaque timer peut compter de 0 à 65 535. La fréquence d'un timer se choisit parmi les quatre disponibles :
- La fréquence de l'horloge système, ou 16.78 Mhz
- Toutes les 64 pulsations d'horloge, soit toutes les 3.814 microsecondes
- Toutes les 256 pulsations d'horloge, soit toutes les 15.256 microsecondes
- Toutes les 1024 pulsations d'horloge, soit toutes les 61.025 microsecondes
Ainsi, un timer basé sur une fréquence de 1024 pulsations d'horloge atteindra la valeur 16 386 en une seconde. Voyons comment réaliser une petite fonction permettant d'attendre un nombre déterminé de secondes. Le listing numéro deux propose auparavant la définition des troisième et quatrième timers. Les registres intitulés REG_TM*CNT désignent les registres de contrôle des timers, et les registres REG_TM*D accueillent la valeur propre des timers. Nous avons également besoin de définir les bits de contrôle. La définition TIMER_OVERFLOW ouvre la possibilité de faire déborder un timer dans le timer suivant lorsque sa valeur maximale est atteinte.
void
waitSeconds
(
int
n)
{
REG_TM3CNT =
TIMER_FEQUENCY_1024 |
TIMER_ENABLE;
REG_TM3D =
0
;
while
(
n--
)
{
while
(
REG_TM3D <=
16386
);
REG_TM3D =
0
;
}
}
Les instructions attribuant la valeur zéro au registre REG_TM3D entraînent le réenclenchement du timer. En positionnant le bit TIMER_OVERFLOW dans REG_TM3CNT, chaque fois que le timer numéro trois atteindrait la valeur 65 535 (donc REG_TM2D), le timer numéro quatre (donc REG_TM3D) se verrait incrémenté d'une unité, et REG_TM2D reprendrait à zéro. Considérez bien que le flag TIMER_OVERFLOW se positionne sur le registre de contrôle du timer devant recevoir le dépassement de capacité. Cette particularité simplifie considérablement la gestion de longues durées. La désactivation d'un timer s'obtient en attribuant simplement la valeur nulle au registre de contrôle. Reprenons notre fonction concernant les nombres aléatoires.
Le listing numéro trois présente la fonction finale telle que rédigée dans le programme. Après activation des deux timers, nous plaçons le code de la boucle d'attente. Notez que le premier timer emploie une fréquence de 256 pulsations d'horloge tandis que l'autre timer ne précise rien. Il ne se voit employé que par dépassement de capacité. Ensuite nous initialisons la graine de génération de nombres aléatoires par l'intermédiaire de la fonction sgenrand(). Cette fonction se trouve incluse dans le fichier fastrandom.cpp qui propose une implémentation efficace de l'algorithme de génération de nombres aléatoires. Pour ne reposer que sur les librairies du compilateur GCC, nous devrions exécuter la fonction srand(). De même, les différents appels de la fonction genrand() devraient se métamorphoser en appels à rand().
6. Affichage du plasma▲
Le support des touches de la GameBoy Advance nous ouvre les portes du scrolling. Le programme comprend ainsi deux entiers x et y précisant les coordonnées du point de vue de l'image. Par défaut, le coin en haut à gauche de l'écran correspond au même coin au sein du heightmap. Les coordonnées x et y sont relatives au heightmap. Ainsi, se voient-elles comprises en 0 et g->width - 1 pour x et 0 et g->height - 1 pour y, où g incarne un pointeur sur une structure GRID_2D. De ce fait, la fonction d'affichage du terrain devient particulièrement compliquée. La procédure de dessin fonctionne, malgré le DMA, en quatre étapes. Prenons le cas le plus défavorable d'affichage. Ce cas se présente lorsque l'un des coins du heightmaps se trouve affiché au centre exact de l'écran.
Dans ce cas, nous devons procéder à quatre dessins différents, un pour chaque quart de l'écran. En effet, chaque dessin nécessitera une copie DMA en des endroits fort différents de la mémoire. Le troisième schéma de cet article propose une aide pour bien comprendre le découpage en quatre parties distinctes. Les deux quarts du haut sont dessinés, puis les deux quarts du bas. Au final, la fonction d'affichage fonctionne, certes, mais pèche par sa rapidité, en particulier à cause des clauses conditionnelles imbriquées dans les boucles for.
7. Introduction aux sprites▲
La GameBoy Advance possède la faculté matérielle d'afficher des sprites. Un sprite définit un élément graphique généralement en mouvement. Au delà du simple affichage, la GameBoy Advance est apte à gérer de manière matérielle la rotation, le changement d'échelle ou la transparence des sprites.
La gestion des sprites en mémoire se situe à deux niveaux. Le premier concerne la mémoire OAM (Objet Attribute Memory). Le second niveau concerne la mémoire des entités. Il s'agit de la mémoire recelant les images bitmap des sprites. L'OAM recense les attributs de chaque sprite. A tout moment, l'OAM, qui peut se voir manipulée en tant qu'un vecteur, peut contenir 128 sprites. Le stockage des entrées débute à l'adresse 0x70000000 et occupe 1 kilo-octet. Les attributs des sprites placées dans l'OAM permettent de définir la position, la taille, le nombre de couleur et la rotation. Le listing numéro cinq offre un exemple de structure de gestion de l'OAM et des attributs. Nous pouvons constater que nous employons en réalité une "copie" de la mémoire OAM. En effet, l'OAM se trouve protégé lors des périodes de rafraîchissement de l'image. Nous ne pouvons y accéder qu'à la suite d'une synchronisation verticale. De plus, si nous utilisions l'OAM directement, nous pourrions risquer de modifier un sprite en cours d'affichage. Les conséquences seraient désastreuses.
8. Les attributs OAM▲
Avant de manipuler les sprites, il est nécessaire de savoir à quoi servent les différents attributs. Pour le premier attribut :
- bits 0 à 7 : coordonnée y du sprite, valeur comprise entre 0 et 255, 159 représentant le bas de l'écran
- bit 8 : flag de rotation et/ou mise à l'échelle. Lorsque ce flag est mis à vrai, la console emploiera les paramètres correspondants
- bit 9 : ce bit permet de doubler la taille du sprite
- bits 10 et 11 : ces bits concernent l'alpha blending
- bit 12 : flag de mosaïque, nous en reparlerons lors de la découverte des tiles
- bit 13 : concerne le mode de couleur. S'il est mis à 1, le sprite emploie 256 couleurs, 16 sinon
- bits 14 et 15 : la forme du sprite, nous en reparlerons pour traiter l'affichage
Le deuxième attribut concerne les informations suivantes :
- bits 0 à 8 : coordonnée x du sprite, valeur comprise entre 0 et 512, 239 représentant le côté droit de l'écran
- bits 9 à 13 : si le flag de rotation/mise à l'échelle est activé, ces bits définissent l'endroit où aller chercher les données de rotation. Sinon, les bits 9 à 11 sont inusités, le bit 12 incarne le renversement horizontal du sprite et le bit 13 le renversement vertical
- bits 14 et 15 : permet de définir la taille du sprite
Enfin, voici les informations entretenus par le troisième attribut. Nous nos pencherons sur le quatrième attribut lors de l'étude des rotations.
- bits 0 à 9 : indice du premier tile de 8 pixels par 8 du sprite au sein de la mémoire d'entités
- bits 10 et 11 : définit la priorité, les sprites de priorité 0 se verront dessinés par dessus les sprites de priorité 3. Si un sprite possède la même priorité qu'un arrière-plan, le sprite sera dessiné par dessus
- bits 12 à 15 : dans le cas d'un sprite en 16 couleurs, ces bits déterminent les couleurs de la palette
Nous ne continuerons pas l'exploration des sprites ce mois-ci. Néanmoins, le CD-Rom recèle un programme d'exemple manipulant un sprite et des tiles en arrière-plan. Ce logiciel laisse loisir au joueur de pratiquer des rotations et des zoomes sur le sprite ainsi qu'un scrolling de l'arrière-plan. Le programme se voit également accompagné de nombreux en-têtes C glanés sur Internet et donnant accès à de nombreuses possibilités (dessins simplifiés, gestions des timers et du clavier, etc.).
9. Listings▲
Listing 1
#define KEY_A 1
#define KEY_B 2
#define KEY_SELECT 4
#define KEY_START 8
#define KEY_RIGHT 16
#define KEY_LEFT 32
#define KEY_UP 64
#define KEY_DOWN 128
#define KEY_R 256
#define KEY_L 512
Listing 2
#define REG_TM2D *(volatile u16*) 0x4000108
#define REG_TM2CNT *(volatile u16*) 0x400010A
#define REG_TM3D *(volatile u16*) 0x400010C
#define REG_TM3CNT *(volatile u16*) 0x400010E
Listing 3
#define TIME_FREQUENCY_SYSTEM 0x0
#define TIME_FREQUENCY_64 0x1
#define TIME_FREQUENCY_256 0x2
#define TIME_FREQUENCY_1024 0x3
#define TIME_OVERFLOW 0x4
#define TIME_ENABLE 0x80
#define TIME_IRQ_ENABLE 0x40
Listing 4
void
waitKey
(
int
whatKey)
{
REG_TM2CNT =
FREQUENCY_256 |
TIMER_ENABLE;
REG_TM3CNT =
TIMER_OVERFLOW |
TIMER_ENABLE;
while
(
1
)
{
if
(!(*
KEYS &
whatKey))
break
;
}
sgenrand
(
REG_TM2D);
REG_TM2CNT =
0
;
REG_TM3CNT =
0
;
REG_TM2D =
0
;
REG_TM3D =
0
;
}
Listing 5
typedef
struct
tSprite
{
u16 attribute0;
u16 attribute1;
u16 attribute2;
u16 attribute3;
}
Sprite;
Sprite OAMBuffer[128
];