Développer en Amateur sur Nintendo DS

Exemple pas à pas

La base

Le template de la PALib se présente de la façon suivante :

// Includes
#include <PA9.h> // Include for PA_Lib

// Function: main()

int main(int argc, char ** argv) {
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL

  // Infinite loop to keep the program running
  while (1) {
    PA_WaitForVBL();
  }

  return 0;
} // End of main()

On y remarque essentiellement 4 points importants :

Le VBL est une interruption système qui arrive à chaque nouvelle image. Cela permet de mettre à jour les informations liées au boutons, au stylet et aux sprites.

Comme on peut le voir, le code de base est très simple. Il ne nous reste plus qu'à y ajouter notre propre code.

Afficher un fond

La première étape va être pour nous d'afficher les décors de notre jeu. Nous allons utiliser 3 images superposées afin d'obtenir un effet de profondeur.

Voici nos 3 images (cliquez dessus pour télécharger les images à la taille réelle) :
Plateformes Montagnes Nuages

Avant toute chose, il faut savoir que la Nintendo DS ne peut pas utiliser directement ces bitmaps comme décors. Il va falloir les convertir en un format compréhensible par la console.
La PALib est fournie avec un utilitaire qui se charge de faire ça. Il s'agit de PAGfx. Ce logiciel, écrit en C#, prend une liste de bitmaps ainsi qu'une couleur à remplacer par de la transparence et génère en retour les fichiers appropriés pour la console.
Nos bitmaps utilisent la couleur magenta (le rose violacé) comme couleur transparente. Pour le spécifier à PAGfx vous pouvez soit utiliser l'interface graphique de PAGfx (Windows seulement) soit spécifier ça dans un fichier ini et lancer la version en ligne de commande de PAGfx.

Voici le fichier ini a utiliser pour convertir nos décors (téléchargeable ici) :

#TranspColor Magenta

#Backgrounds :
bg0.bmp EasyBg bg0
bg1.bmp EasyBg bg1
bg2.bmp EasyBg bg2

La spécification d'un décors se fait par 3 mots clef : le nom du fichier sur le disque, le type de décors et le nom de la palette de couleur.
Dans le code du jeu, il vous devrez utiliser le nom du fichier comme identifiant pour l'image.
Concernant les types de décors, vous avez le choix entre :

Une fois les images converties, copiez soit les fichiers .c dans votre répertoire de source, soit les fichiers .bin dans votre répertoire "data".
Pour pouvoir les utiliser dans notre code, il nous faut inclure le fichier généré nommé "all_gfx.h" qui contient les déclarations de toutes les images spécifiées dans PAGfx.ini.

Maintenant, place au code pour afficher les décors (les nouvelles portions de code sont surlignées) :

// Includes
#include <PA9.h> // Include for PA_Lib
#include "all_gfx.h" // Include all our graphicals objects

// Function: main()

int main(int argc, char ** argv) {
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL

  // Loads the backgrounds
  PA_DualLoadPAGfxLargeBg(1, bg0);
  PA_DualLoadPAGfxLargeBg(2, bg1);
  PA_DualLoadPAGfxLargeBg(3, bg2);

  PA_DualInitParallaxX(0, 256, 128, 64); // Initializes the parallax effect

  // Infinite loop to keep the program running
  while (1) {
    PA_WaitForVBL();
  }

  return 0;
} // End of main()

Ce code contient donc 3 points importants :

La PALib regorge de fonctions pour charger ses décors. Dans l'exemple, nous utilisons PA_DualLoadPAGfxLargeBg car nous chargeons un décors de type "LargeBg" et car nous souhaitons l'afficher en mode "dual", c'est à dire : sur les 2 écrans en même temps (comme si nous n'avions qu'un seul grand écran). Pour plus d'informations sur les méthodes de chargements, vous pouvez consulter la documentation.

La fonction d'initialisation du scrolling différentiel prend 4 arguments qui représentent la vitesse de déplacement de chaque décors. Le premier argument représente la vitesse du décors 0, le deuxième argument la vitesse du décors 1 et ainsi de suite. La vitesse est à spécifier en fixed point.

Dans l'exemple le décors 0 ne se déplacera pas (vitesse nulle), le décors 1 (les plateformes) se déplacera à une vitesse normale (256/256 = 1), le décors 2 (les montagnes) se déplacera 2 fois moins vite (128/256 = 0,5) et le décors 3 (les nuages) se déplacera 4 fois moins vite que la normale (64/256 = 0,25).

Faire du scrolling

Le scrolling est très simple à faire avec la PALib. Tout (ou presque) est déjà inclus de base, il nous vous reste plus qu'à utiliser les fonctions de la bibliothèque.

Il existe 2 fonctions principales pour le scrolling : PA_ParallaxScrollX et PA_"Mode"Scroll. Ces 2 méthodes sont ensuite déclinées (en X et en Y pour ParallaxScroll ou avec le nom du mode correspondant pour PA_"Mode"Scroll) mais la façon de les utiliser reste identique.

Revoici donc notre code après l'ajout de quelques instructions pour le scrolling :

// Includes
#include <PA9.h> // Include for PA_Lib
#include "all_gfx.h" // Include all our graphicals objects

// Function: main()

int main(int argc, char ** argv) {
  s32 x_scrollpoint = 0;

  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL

  // Loads the backgrounds
  PA_DualLoadPAGfxLargeBg(1, bg0);
  PA_DualLoadPAGfxLargeBg(2, bg1);
  PA_DualLoadPAGfxLargeBg(3, bg2);

  PA_DualInitParallaxX(0, 256, 128, 64); // Initializes the parallax effect

  // Infinite loop to keep the program running
  while (1) {
    if(Pad.Held.Left)
      PA_DualParallaxScrollX(--x_scrollpoint);
    else if(Pad.Held.Right)
      PA_DualParallaxScrollX(++x_scrollpoint);
   
    PA_WaitForVBL();
  }

  return 0;
} // End of main()

Ce morceau de code est l'occasion de voir rapidement Pad.Held qui permet de vérifier si le joueur appuie sur une touche. Nous verrons ce mécanisme plus en détail dans la partie "déplacer un sprite".
En dehors de ceci, nous pouvons voir 2 points intéressants :

Afficher un sprite

Les sprites sont gérées en hardware par la Nintendo DS, l'affichage d'un sprite à l'écran se fait donc très simplement.
La console enregistre principalement les informations suivantes pour chaque sprite :

La Nintendo DS gère jusqu'à 128 sprites par écran. Un même sprite ne peut pas se déplacer d'un écran à l'autre. Pour donner cette impression, une solution consiste à allouer tous les sprites sur les 2 écrans en même temps. C'est d'ailleurs ce que fait la PALib avec ses fonctions "dual".

Comme pour les décors, nous devons commencer par convertir l'image de notre sprite au format de la Nintendo DS.
Voici l'image de notre héro (cliquez dessus pour télécharger l'image complète) :
Tux Bross

Et revoici notre fichier PAGfx.ini avec, en plus, la ligne pour convertir cette image :

#TranspColor Magenta

#Sprites :
tux.bmp 256colors palette

#Backgrounds :
bg0.bmp EasyBg bg0
bg1.bmp EasyBg bg1
bg2.bmp EasyBg bg2

La spécification d'un sprite se fait par 3 mots clef : le nom du fichier sur le disque, le nombre de couleurs et le nom de la palette.
Le nombre de couleurs peut être soit "256colors", soit "16colors".

Nous allons ajouter quelques lignes à notre code pour afficher notre héro :

[...]

// Function: main()
int main(int argc, char ** argv) {
  s32 x_scrollpoint = 0;

  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL

  // Loads the backgrounds
  PA_DualLoadPAGfxLargeBg(1, bg0);
  PA_DualLoadPAGfxLargeBg(2, bg1);
  PA_DualLoadPAGfxLargeBg(3, bg2);

  PA_DualInitParallaxX(0, 256, 128, 64); // Initializes the parallax effect

  // Loads player's sprite
  PA_DualLoadSpritePal(0, (void*) palette_Pal);
  PA_DualCreateSprite(0, (void*) tux_Sprite, OBJ_SIZE_32X32, 1, 0, 96, 80);

  // Infinite loop to keep the program running
  while (1) {

  [...]

Les 2 lignes ajoutées correspondent à :

Animer un sprite

Avec la PALib, l'animation d'un sprite fonctionne essentiellement grâce à 3 fonctions : PA_StartSpriteAnim, PA_StopSpriteAnim et PA_SetSpriteAnimFrame.

  [...]

  // Loads player's sprite
  PA_DualLoadSpritePal(0, (void*) palette_Pal);
  PA_DualCreateSprite(0, (void*) tux_Sprite, OBJ_SIZE_32X32, 1, 0, 96, 80);

  PA_DualStartSpriteAnim(0, 0, 1, 6);

  // Infinite loop to keep the program running
  while (1) {

  [...]

Déplacer un sprite

Voici la dernière étape de cet exemple, nous allons faire marcher et sauter notre sprite.

Nous utiliserons les informations provenant des boutons et de la croix de direction pour contrôler le personnage. Ces informations sont stockées dans la variable globale "Pad".
Pour détecter l'emplacement des plateformes, nous lirons directement le contenu de l'image "bg0" (il s'agit de l'image contenant les plateformes).

Nous allons avoir besoin de plusieurs variables pour gérer les mouvements de notre sprite. Nous stockerons ces informations dans une structure nommée "character".

// Includes
#include <PA9.h> // Include for PA_Lib
#include "all_gfx.h" // Include all our graphicals objects


typedef struct {
  u8 sprite_id;
  s16 x;
  s32 y; // This value will be stored in fixed point
  s32 fall_speed; // This value will be stored in fixed point
  u8 moving;
  u8 moving_prev;
  u8 jumping;
} character;

// Function: main()
int main(int argc, char ** argv) {
  s32 x_scrollpoint = 0;
  s16 layer = 0;
  character player;

  const u8 MAP_WIDTH = bg0_Info[1]/8;
  const u8 BLANK_TILE = bg0_Map[0];

  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL

  // Loads the backgrounds
  PA_DualLoadPAGfxLargeBg(1, bg0);
  PA_DualLoadPAGfxLargeBg(2, bg1);
  PA_DualLoadPAGfxLargeBg(3, bg2);

  PA_DualInitParallaxX(0, 256, 128, 64); // Initializes the parallax effect

  // Settings the player's attributes
  player.sprite_id = 0;
  player.x = 32;
  player.y = 32<<8;
  player.fall_speed = 0;
  player.moving = 0;
  player.jumping = 0;

  // Loads the player's sprite
  PA_DualLoadSpritePal(0, (void*) palette_Pal);
  PA_DualCreateSprite(player.sprite_id, (void*) tux_Sprite, OBJ_SIZE_32X32, 1, 0, 112, (player.y>>8)-16);

  // Infinite loop to keep the program running
  while (1) {

    player.moving = 0;
    player.moving_prev = player.moving;

    // Moves the sprite
    if(Pad.Held.Left) {
      // If there is no wall on the left, move the player
      if(bg0_Map[(player.x-8)/8 + (((player.y >> 8)+24)/8)*MAP_WIDTH] == BLANK_TILE)
        player.x -= 2;

      player.moving = 1;
      PA_DualSetSpriteHflip(player.sprite_id, 1); // Flip the sprite horizontally to make it look to the left
    }
    else if(Pad.Held.Right) {
      // If there is no wall on the right, move the player
      if(bg0_Map[(player.x+8)/8 + (((player.y >> 8)+24)/8)*MAP_WIDTH] == BLANK_TILE)
        player.x += 2;
      player.moving = 1;
      PA_DualSetSpriteHflip(player.sprite_id, 0);
    }

    // If the tile located right under the player isn't empty (!= BLANK_TILE), stop the fall (fall_speed = 0)
    if(bg0_Map[player.x/8 + (((player.y >> 8)+16)/8)*MAP_WIDTH] != BLANK_TILE) {
      player.fall_speed = 0;
      if(player.jumping)
        player.moving_prev = 0; // Asks to start the "moving" animation
      player.jumping = 0;
    }
    // Otherwise, increase its fall speed (unless it is greater than 3px per frame)
    else if(player.fall_speed < 768)
      player.fall_speed += 32;

    player.jumping = player.jumping || player.fall_speed != 0;

    // If the player press the A button, make it jump
    if(Pad.Newpress.A && player.jumping == 0) {
      // Setting a negative value for the fall speed will make the sprite falling upward, thus jumping ;-)
      player.fall_speed = -1024;
      player.jumping = 1;
    }

    // Collision between the hero's head and a block
    if(player.fall_speed < 0 && bg0_Map[player.x/8 + ((player.y >> 8)/8)*MAP_WIDTH] != BLANK_TILE)
      player.fall_speed = 0;

    // Collision with the top of the screen
    if(player.y < 0 && player.fall_speed < 0)
      player.fall_speed = 0;

    // Moves the hero vertically
    player.y += player.fall_speed;
    PA_DualSetSpriteY(0, (player.y >> 8));

    // Animating the player's sprite
    if(player.jumping)
      PA_DualStartSpriteAnim(player.sprite_id, 2, 2, 1);
    else if(player.moving && !player.moving_prev)
      PA_DualStartSpriteAnim(player.sprite_id, 0, 1, 6);
    else if(!player.moving)
      PA_DualStartSpriteAnim(player.sprite_id, 0, 0, 1);

    // When the player reach one side of the map, teleports him to the other side
    if(player.x > bg0_Info[1]) {
      player.x -= bg0_Info[1];
      layer++;
    }
    else if(player.x < 0) {
      player.x += bg0_Info[1];
      layer--;
    }

    // Updating the scrollpoint
    x_scrollpoint = player.x - 128 + layer*bg0_Info[1];
    PA_DualParallaxScrollX(x_scrollpoint);

    PA_WaitForVBL();
  }

  return 0;
} // End of main()

Donc cette fois-ci, il y a beaucoup de nouveautés (au moins en apparence) dans le code. Voici les nouveaux points :