IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Programmation de la Game Boy Advance : affichage de tiles 3/3

Noël ! Nous allons vous faire découvrir le plus précisément possible les procédés avancés de manipulation des backgrounds. Nous allons ainsi apprendre à déplacer, à faire pivoter et à changer l'échelle d'un background.

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

1. Introduction

Au même titre que les sprites, les cartes de tiles se révèlent capables de transformations géométriques. Nous allons devoir toutes les étudier en même temps car celles-ci se trouvent étroitement liées par des calculs mathématiques.

2. Déplacement d'un background texte

L'étude du mois dernier a démontré que les backgrounds existaient sous deux formes distinctes : les backgrounds en mode texte et ceux en mode rotation. Si la manipulation des seconds requière une pléthore de registre, les ensembles de tiles en mode texte ne nécessitent l'emploi que de quatre registres. Les seules transformations autorisées sur ces ensembles sont les déplacements verticaux et horizontaux définis respectivement par les registres REG_BGxHOFS et REG_BGxVOFS (le petit x symbolise le numéro du background à considérer).

Le contenu de ces registres de 16 bits désigne l'origine utilisée dans la carte lors de l'affichage. L'unité de valeur de ces registres est le pixel et seules des valeurs entre 0 et 255 sont admises. En effet, parmi les 16 bits dédiés, seulement 8 sont accessibles. En outre, ces registres ne sont accessibles en lecture seule. Il est donc impératif pour le programmeur de conserver les informations de position du background quelque part. En l'occurrence, nous avons modifié la structure t_Background pour lui ajouter les attributs xScroll et yScroll.

 
Sélectionnez
/* Listing 1 */
typedef struct t_Background
{
  u16* tileData;
  u16* mapData;                   
  u16 size; 
  u8 mosaic,
     colorMode,
     number, 
     charBaseBlock,
     screenBaseBlock,
     wraparound;
  s16 xScroll,
      yScroll,
      PA, PB, PC, PD;
  s32 DX, DY;
} Background, *pBackground;

Nous devons donc implémenter une recopie de ces valeurs dans les registres adéquats afin d'enclencher le déplacement. En considérant un background numéro 2 dont le contexte graphique se veut le Mode 0, nous pouvons rédiger les lignes suivantes :

 
Sélectionnez
REG_BG2HOFS = bg->xScroll;
REG_BG2VOFS = bg->yScroll;

Le programme d'exemple offre une fonction spécifique intitulée updateBackground(). Le rôle de cette dernière consiste à mettre à jour les registres correspondant au background passé en paramètre. Le seul réel intérêt d'un point de vue théorique de ce fragment de code réside dans le cas des backgrounds 2 et 3. Ces derniers sont normalement en mode rotation, sauf quand le mode graphique est zéro. Le déplacement d'un background en mode rotation implique la mise en ouvre de 6 registres supplémentaires.

3. Les backgrounds de rotation

Pour déplacer un background en mode rotation, le développeur doit nécessairement tenir compte de l'angle de rotation de celui-ci mais également du "zoom". Lors de notre apprentissage de la rotation des sprites, nous avions découvert la nécessité d'utiliser quatre valeurs correspondant aux quatre coins du rectangle entourant le sprite. Le principe est similaire en ce qui concerne les backgrounds. Toutefois, plutôt que d'utiliser une subtilité de cartographie de la mémoire, nous pouvons employer directement quatre registres nommés REG_BGxPA, REG_BGxPB, REG_BGxPC et REG_BGxPD. Ces registres de 16 bits, spécifiques aux backgrounds 2 et 3 seront sujets à des manipulations trigonométriques. Ces dernières, pour des raisons de performances, repose sur des tables de cosinus et de sinus pré calculées. Nous allons encore plus loin puisque ces tables ne sont pas calculées dans notre programme, mais par un programme externe générant un code source C contenant la définition complète de ces tables.

Le déplacement en lui-même correspond à la modification de deux registres particuliers, désignés sous les noms de REG_BGxX et de REG_BGxY. Accueillant 16 bits, nous pouvons placer au sein de ceux-ci le point de référence avant rotation, c'est-à-dire la position du background. Un peu de mathématiques nous permettront de provoquer des déplacements relatifs à l'angle de rotation de la carte et non par rapport à l'écran physique de la console (à l'inverse du sprite). Le schéma 1 illustre ce principe. Comme leurs pendants pour les backgrounds en mode texte, ces registres ne sont accessibles qu'en écriture. Nous devons donc ajouter des attributs à la structure représentant les cartes.

Schéma 1. Déplacements relatifs des cartes de tiles.
Schéma 1. Déplacements relatifs des cartes de tiles.

Toutes les transformations géométriques se trouvent effectuées par rapport à un point d'origine particulier, que nous appellerons le centre. Le listing numéro 2 présente la fonction rotateBackground() chargée de mener à bien toutes ces opérations. La première étape concerne à ajuster la valeur des coordonnées du centre de transformation : center_x et center_y. Ce point doit se trouver dans l'écran, donc entre (0, 0) et (240, 160). L'ajustement est réalisé en fonction de la valeur du zoom, l'absence de zoom étant caractérisé par l'usage de la valeur (1 << 8). Les décalages incessants vers la gauche ou vers la droite de 8 bits s'expliquent par le fait que la GameBoy Advance, pour favoriser un contrôle extrêmement précis du zoom, emploie des nombres à virgule fixe. La partie fractionnelle de ces nombres correspond aux 8 bits de poids faibles. De ce fait, un zoom nul correspond à la valeur décimale 1.0 que nous représentons par 1 << 8 (la partie fractionnelle vaut zéro).

 
Sélectionnez
/* Listing 2 */
void rotateBackground(pBackground bg, int angle, int center_x, int center_y, s32 zoom)
{
  center_y = (center_y * zoom) >> 8;
  center_x = (center_x * zoom) >> 8;

  bg->DX = ((bg->xScroll << 8) - center_y * SIN[angle] - center_x * COS[angle]);
  bg->DY = ((bg->yScroll << 8) - center_y * COS[angle] + center_x * SIN[angle]);

  bg->PA = ( COS[angle] * zoom) >> 8;
  bg->PB = ( SIN[angle] * zoom) >> 8;
  bg->PC = (-SIN[angle] * zoom) >> 8;
  bg->PD = ( COS[angle] * zoom) >> 8;
}

Une fois ajustées, ces coordonnées interviennent dans le calcul des valeurs des registres REG_BGxX et REG_BGxY. En utilisant des déplacements absolus (donc relatifs à l'écran et non à l'angle et au centre de rotation), le calcul se révèlerait très concis :

 
Sélectionnez
bg->DX = bg->xScroll << 8;
bg->DY = bg->yScroll << 8;

Le décalage binaire s'explique de la même manière que précédemment : nos registres font appel à une partie fractionnelle codée sur un octet. Néanmoins, nous souhaitons pouvoir procéder à un déplacement relatif. C'est pourquoi un brin de trigonométrie apparaît afin d'appliquer un décalage supplémentaire à nos coordonnées.

Enfin, cette fonction réalise le calcul de la matrice de transformation finale (définie par les registres PA, PB, PC et PD). Les détails des formules mathématiques implémentées dans la fonction rotateBackground() se trouvent présentés sur le schéma 2. Sur ce schéma, les coordonnées (x0, y0) désignent le centre de rotation, (x1, y1) le point d'origine, (x2, y2) le résultat de la transformation, ? l'angle de rotation, ? le zoom sur l'axe horizontal et ? le zoom sur l'axe verticale. Dans la pratique, ? et ? sont égaux.

Schéma 2. Les formules de transformations des backgrounds.
Schéma 2. Les formules de transformations des backgrounds.

4. Utilisation des transformations

L'implémentation de nos nouvelles fonctions nécessite d'appliquer quelques menus changements à notre code source. Par exemple, l'invocation de la méthode enableBackground() doit impérativement avoir lieu tout de suite après la synchronisation verticale, soit l'appel de waitForVSync(). Dans la boucle d'affichage nous ajoutons un appel à rotateBackground() :

 
Sélectionnez
rotateBackground(bg2, angle, SCREEN_WIDTH >> 1, SCREEN_HEIGHT >> 1, zoom);

Le centre de rotation pour lequel nous avons ici opté se trouve le centre physique de l'écran. Ceci a une influence particulière sur l'initialisation du background bg2. En effet, puisque les déplacements s'effectuent relativement au centre de rotation, le coin supérieur gauche, l'origine, de notre carte sera présentée au centre de l'écran au démarrage du jeu. Pour y remédier nous devons initialiser les valeurs de déplacement correctement, c'est-à-dire en leur donnant pour valeur la moitié de la résolution :

 
Sélectionnez
bg2->xScroll = SCREEN_WIDTH  >> 1;
bg2->yScroll = SCREEN_HEIGHT >> 1;

Ainsi, l'origine de la carte coïncide avec l'origine de l'écran. Ce problème apparaît également dans la fonction getInput(). Lorsque nous déplaçons le sprite jusqu'au bord de l'écran nous activons le défilement dans la direction souhaitée. Néanmoins, pour interdire au joueur de sortir des limites de la carte de jeu, le développeur doit prendre en compte les coordonnées du centre de rotation. Par exemple le listing numéro 3 présente le code source de navigation vers la droite.

 
Sélectionnez
/* Listing 3 */
if (!(*KEYS & KEY_RIGHT))
{
  if (xwing->x < SCREEN_WIDTH - xwing_WIDTH)
    xwing->x++;
  else if (bg2->xScroll + (SCREEN_WIDTH >> 1) < 512)
    bg2->xScroll++;
}

Ainsi rédigées, ces lignes n'autorisent pas le joueur à dépasser la limite droite du background. Vous noterez que pour ce faire, nous introduisons un décalage supplémentaire correspondant à la moitié de la largeur de l'écran. La comparaison est effectuée avec la valeur de référence 512 : rappelez-vous que notre background a été définit avec une taille de 512 pixels par 512. La gestion des déplacements vers la gauche, le haut et le bas se veut comparable à celle-ci.

Jusqu'à présent, notre programme octroyait l'opportunité de faire tourner le sprite par l'entremise des touches A et B de la machine. Nous allons profiter de l'attribut d'angle de rotation du sprite pour appliquer des rotations à notre décor. Dans l'appel de rotateBackground(), nous remplaçons alors la variable "angle" par l'instruction suivante :

 
Sélectionnez
xwing->angle == 0 ? 0 : 359 - xwing->angle)

Dorénavant, les tiles tourneront dans le sens inverse du sprite. En faisant tourner de quelques dizaines de degrés le sprite puis en le déplaçant, vous constaterez un phénomène curieux. Le déplacement absolu du sprite entraîne le déplacement relatif du décor. En vous basant sur les formules évoquées précédemment, vous devriez pouvoir aisément modifier la trajectoire du sprite pour la rendre également relative à son angle de rotation.

Puisque nous venons d'apprendre à utiliser le zoom, pourquoi ne pas l'implémenter également ? Cette fois-ci, l'effet ne sera pas contrôlé par le joueur mais totalement automatisé. Pour commencer, nous déclarons deux variables :

 
Sélectionnez
s32 zoom = 1 << 8;
s8  inout = -2;

La première de ces deux variables contient le coefficient de zoom dont la valeur par défaut se trouve être 1.0. La seconde variable définit le coefficient de grossissement. En l'occurrence, il s'agit d'un grossissement négatif qui rétrécira le champ de vision. Lorsque notre zoom atteint une valeur limite le coefficient est inversé afin d'obtenir un effet de "rebond".

 
Sélectionnez
zoom += inout;
if (zoom <= 2)
  inout = 2;
else if (zoom >= (1 << 8))
  inout = -2;

Gardez bien à l'esprit que le zoome repose sur un nombre à virgule fixe. De fait, le test if (zoom <= 2) ne signifie pas que nous inversons le grossissement quand le facteur de zoom devient inférieur ou égal à 2.0.

5. Mosaïque

Un effet particulièrement intéressant se trouve câblé au cour de la GameBoy Advance, le mode mosaïque. Ce mode est généralement employé par les programmeurs pour réaliser des effets de transition entre deux écrans dans les menus ou entre les menus et le jeu. Le mode mosaïque permet de "pixelliser" une surface en la subdivisant en rectangles de dimensions précises. Le registre responsable de cet effet se nomme REG_MOSAIC. Occupant 16 bits, ce registre ce décompose en quatre sous-ensemble : deux pour les objets (sprites) et deux pour les backgrounds. Parmi ces deux, un correspond à la résolution verticale de la mosaïque et l'autre à la résolution horizontale. Ainsi, chaque résolution occupe 4 bits et peut se voir attribué une valeur entre 0 et 15. Les deux lignes de code suivantes définissent le registre approprié ainsi qu'une macro permettant de manipuler les résolutions aisément :

 
Sélectionnez
#define REG_MOSAIC *(u32*) 0x400004C
#define SetMosaic(bh, bv, oh, ov) ((bh) + (bv << 4) + (oh << 8) + (ov << 12))

Malgré cela, l'instruction REG_MOSAIC = SetMosaic(8, 8, 0, 0) ne fonctionnera pas. Il est nécessaire de préciser auparavant que nous souhaitons activer le mode mosaïque. Pour ce faire, nous utilisons le drapeau BG_MOSAIC_ENABLE lors de la création de notre background :

 
Sélectionnez
bg2->mosaic = BG_MOSAIC_ENABLE;

Fait intéressant, le mode mosaïque s'adapte également au mode 4 pour l'affichage d'une simple image. Ainsi, nous pouvons créer une belle transition depuis notre écran titre (voir listing 4) qui remplace le précédent fondu au noir. Après avoir activé le drapeau dans le registre REG_BG2CNT, nous augmentons progressivement la résolution de la mosaïque en allant de la plus faible (0) à la plus forte (15). Identiquement à notre effet de fondu au noir, cet algorithme exploite la fonction waitTime() pour ralentir l'effet. La durée totale de l'effet choisie correspond à une seconde.

L'effet de transition doit, pour être achevé, comprendre une symétrie de notre augmentation de résolution. Le début de la fonction startGame() comprend une boucle similaire à celle découverte dans le listing 4.

 
Sélectionnez
/* Listing 4 */
void introduction()
{
  REG_DISPCNT = MODE_4 | BG2_ENABLE;
  REG_BG2CNT = BG_MOSAIC_ENABLE;
 
  displayPicture(retaliatorData, retaliatorPalette);
  while (*KEYS & KEY_START);

  for (s16 mosaic = 0; mosaic < 16; mosaic++)
  {
    REG_MOSAIC = SetMosaic(mosaic, mosaic, 0, 0);
    waitTime(0, 1000 / 16);
  }
}

L'instruction suivant immédiatement la boucle se révèle très intéressante :

 
Sélectionnez
REG_DISPCNT |= OBJ_ENABLE;

Normalement, la couche d'affichage des objets est activée dès la mise en place du mode graphique 2. Or, nous avons ici décidé de la désactiver avant notre effet de mosaïque puis de l'activer. Il est avéré que lors d'un effet de mosaïque, la couche des objets peut provoquer des interférences et autres désagréments visuels. Une seconde solution consisterait à activer le mode mosaïque de la couche des objets en appliquant le drapeau MOSAIC à notre sprite. Dans ce cas, nous invoquerions la macro SetMosaic de la sorte :

 
Sélectionnez
SetMosaic(mosaic, mosaic, mosaic, mosaic)

Les backgrounds autorisent enfin la réalisation d'effets de transparence, extrêmement pratiques pour simuler une brume dans un jeu par exemple. Nous aborderons cet effet dans le prochain article. Nous verrons également le mois prochain comment jouer des sons sur la GameBoy Advance avec les différents canaux et le mode DirectSound. Nous étudierons à ce sujet une librairie qui confère au programmeur la possibilité de jouer des modules sonores au format .mod.

 
Sélectionnez
/* Listing 5 */
// macros de manipulation des mosaïques
#define MOS_BG_HOR(n) (n)
#define MOS_BG_VER(n) (n << 4)
#define MOS_OBJ_HOR(n) (n << 8)
#define MOS_OBJ_VER(n) (n << 12)

6. Téléchargements

Nous vous recommandons de télécharger l'exemple associé à cet article.

Version PDF de l'article (83 Ko).

Liens :

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2006 Romain Guy. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.