:: PlayerAdvance.org ::  

Précédent   :: PlayerAdvance.org :: > :: Développement Amateur :: > Tutoriels

Publicité

Réponse
 
Outils de la discussion Modes d'affichage
Vieux 25/03/2018, 15h28   #1
PypeBros
Membre confirmé
 
Date d'inscription: 15/10/2007
Messages: 69
Par défaut (jour 4) faire ses jeux sur DS avec libGEDS

Salut à tous. Vous le savez déjà, je fais des programmes et des jeux sur Nintendo DS. L'idée, c'est que tout ce que j'ai fait devrait permettre à d'autres d'en faire autant. J'ai commencé une repasse sur mon code pour montrer petit à petit comment se servir de mes outils et de mon moteur de jeu.

Pour ça, il vous faudra d'abord le kit de développement non-officiel pour NDS et GBA. Ils ont fait du bon travail et il y a un installeur automatique pour ceux qui travaillent sous Windows.

De mon côté, j'ai préparé une structure d'accueil pour les programmes. Règles de génération des ROMs nds à partir des sources et des données du jeu, les petites bibliothèques supplémentaires pour faire de la musique, etc. Et un un premier programme à faire tourner sur DS. Evidemment, on est sur une machine "bare metal", sans système d'exploitation, donc il y a un peu plus de code d'initialisation qu'à l'habitude, mais le fait de créer un objet "Engine" fait déjà une grande part du travail.

Code:
void setactive() {
    FileDataReader fd("efs:/sndtrk.xm");
    ntxm9->stop();
    u16 err = ntxm9->load(&fd);
    ge.setWindow(active);
    if (err!=0) iprintf("ntxm says %x\n",err);
}
Un premier morceau, qui charge un morceau de musique (on en trouve d'autres du genre sur modarchive.org). Il sera joué sur le processeur secondaire de la console, l'ARM7. L'objet ntxm9 nous permettra de transmettre des commandes du genre "passe moi la fanfare de fin de niveau" ou "joue un bruit d'explosion" au fil du jeu.

Je propose deux manière d'interagir avec les boutons et l'écran tactile de la console: l'un pour la partie "action" du jeu (pas encore dans le tuto), et l'autre qui me sert pour les éditeurs: ge.setWindow() a défini notre objet window comme étant l'activité actuelle, donc le moteur appellera la fonction ci-dessous chaque fois qu'on pousse sur un des boutons.
Code:
bool handle(uint& keys, Event evt) {
    if (keys & KEY_A) {
      ntxm9->play();
    }
    if (keys & KEY_Y) {
      ntxm9->stop();
    }
    return true;
}
Allez, la prochaine fois je vous raconterai comment mettre des images à l'écran

Dernière modification par PypeBros ; 04/04/2018 à 22h58.
PypeBros est déconnecté   Réponse avec citation

Publicité

Vieux 25/03/2018, 15h30   #2
PypeBros
Membre confirmé
 
Date d'inscription: 15/10/2007
Messages: 69
Par défaut

Bon, avant de voir comment le moteur 'GEDS' peut charger des images dans la mémoire vidéo de la Nintendo DS, il faut que je vous explique un peu comment son processeur graphique construit les images que l'on voit à l'écran. J'utilise le mode "texte", ici, où les images sont composées d'élément de 8x8 pixels appelés "tiles" (prononcez 'taïlesz') que l'on dispose sur une grille.

Vous avez tous déjà utilisé un éditeur de donjons RPG où l'on travaille carré par carré (je crois qu'on dis volontiers des 'chips' ?) plutôt que de mettre un arbre ou une maison d'un coup. Le mode texte de la DS va un cran plus loin: la mémoire de la machine elle-même est découpée en une région pour retenir des pavés de 8x8 (le 'tileset') et une région pour indiquer quel pavé montrer à quel endroit (la 'map' de l'écran, couvrant généralement 256x256 pixels pour la DS). Parfois, on mettra le même tile à plusieurs endroit de l'écran (en inscrivant juste son numéro dans plusieurs cases de la map), et parfois un tile ne sera pas repris (pour l'instant). C'est une technique a peu près aussi vieille que les machines 8-bit et qui a perduré pendant toute la période 16-bit (mais ça, c'est une autre histoire). Ce qui a changé au fil du temps, c'est le nombre de couleurs autorisées par tile (de 3 pour la NES à 255 pour la NDS) et le nombre de calques que l'on peut superposer pour obtenir l'image souhaitée (jusqu'à 4 sur la NDS).

Les fichiers .spr créés par le SpriteEditor pour DS sont principalement une sauvegarde du contenu de la mémoire vidéo tel qu'on voudrait l'avoir pendant le jeu. Du coup, il n'y a vraiment pas grand-chose à faire pour pouvoir les importer dans son programme.
Code:
  SpriteRam myRam(WIDGETS_CHARSET(512));
  SpriteSet mySet(&myRam, BG_PALETTE);

  mySet.Load("efs:/bg.spr");
Et voilà. quelques précisions pour dire au moteur de jeu où mettre les graphismes (ici, sur l'écran "du haut", en laissant 512 emplacement pour une police de caractère et pour les 'maps' des différents calques) et où mettre la palette de couleur présente dans le fichier .spr (également dans la mémoire de palette de l'écran supérieur). La classe SpriteSet du moteur de jeu s'occupe de manipuler le fichier et d'en reconnaître le format. On pourrait aussi prendre le contrôle de toute la mémoire vidéo en utilisant directement myRam(BG_GFX), mais c'est un peu moins confortable pour la suite.

Bon, malheureusement, avoir des graphismes en mémoire ne suffit pas pour les avoir à l'écran. La démo n°2 doit aussi configurer la puce graphique pour qu'elle utilise nos données et remplir la grille/map d'un des calques.

Code:
REG_DISPCNT|=DISPLAY_BG2_ACTIVE;
REG_BG2CNT=BG_MAP_BASE(GEBGROUND)|BG_TILE_BASE(0)|BG_COLOR_256;
La configuration, on va régler ça en programmant directement les registres de la puce graphique. C'est tellement facile sur DS . J'active le bit correspondant au calque n° 2 (j'l'aime bien, celui-là) dans le contrôle principal de l'écran (REGister_DISPlayCoNTrol), puis je donne les paramètres de ce calque dans son registre à lui: les données des tiles seront interprétées comme ayant 256 couleurs, quelle grille utiliser comme map, et si je veux ajouter automatiquement un décalage à tous les numéros de tiles de la map (BG_TILE_BASE) par rapport à la position dans la mémoire du tileset (ce que je ne fais pas ici).

La bibliothèque 'libgeds' donne des paires de noms à différentes grilles: un nom pour la configuration de BG_MAP_BASE, et un nom pour le tableau correspondant dans la mémoire vidéo (WIDGETS_BACKGROUND). Du coup avec la configuration qu'on vient de faire, on peut changer le pavé en haut à gauche de l'écran en écrivant dans WIDGETS_BACKGROUND[0]. Dans celui en haut à droite avec WIDGETS_BACKGROUND[31] (parce qu'il y a 32x8 dans 256 ) et WIDGETS_BACKGROUND[32] sera le pavé juste en-dessous de WIDGETS_BACKGROUND[0]. Et ainsi, ligne après ligne. Bref, avec un peu de sucre et une feuille de papier, vous arriverez rapidement à voir que WIDGETS_BACKGROUND[32*ligne+colonne] vous permet de modifier n'importe quel emplacement de l'écran.

Code:
for (int l=0; l<32; l+=2) {
  for (int b=0; b<8; b+=2) {
    WIDGETS_BACKGROUND[b+l*32]=tile++;
    WIDGETS_BACKGROUND[b+l*32 +1 ]=tile++;
    WIDGETS_BACKGROUND[b+l*32 +32]=tile++;
    WIDGETS_BACKGROUND[b+l*32 +33]=tile++;
  }
}
Moi, j'avais envie de montrer l'ensemble des graphismes de mon fichier .spr, tels qu'on les voit dans l'éditeur. Et dans le quart le plus à gauche de l'écran (une zone de 64x192 pixels). Il reste un petit truc à régler: l'éditeur travaille par blocs de 16x16 pixels, pour le confort du graphiste. En interne, un bloc est composé de 4 tiles de 8x8 pixels, agencés eux aussi suivant le mode "une ligne, puis l'autre". Si je veux que les blocs ne se retrouvent pas coupés en 2, je dois respecter cet agencement au moment de remplir la grille:
Code:
/* 0 1
 * 2 3
 */
D'où les boucles qui passent une ligne sur deux et les quatres écritures à WIDGETS_BACKGROUND par itération.

Voilà. Le code est sur github, donc. Avec le fichier .spr (creative common)


N'hésitez pas à me ralentir avec quelques questions si ça va trop vite pour vous.
PypeBros est déconnecté   Réponse avec citation
Vieux 25/03/2018, 22h33   #3
PypeBros
Membre confirmé
 
Date d'inscription: 15/10/2007
Messages: 69
Par défaut

Un des éléments les plus importants dans un jeu vidéo (2D), ce sont sans doute les sprites. Rien à voir avec une boisson pétillante, il s'agit du terme consacré pour les objets graphiques librement déplaçable à l'écran, y compris le personnage principal, les adversaires, mais aussi certains power-ups ou des effets spéciaux.

Les sprites ont besoin de données graphiques (des valeurs de couleurs) tout comme les décors, et ces données sont de nouveau constituées de "tiles". On va utiliser surtout des blocs de 16x16 pixels ici (et donc constitués de 2x2 tiles) mais la DS peut en réalité utiliser des sprites de 8x8 à 64x64 pisxels (avec quelques restrictions quand-même).

Mettre à jour la mémoire vidéo pour que les sprites s'affichent à l'écran (quels graphismes, quels réglages, etc) est délégué à la classe SpritePage dans la libgeds, et la fonction "sync()" de GuiEngine fera le nécessaire pour mettre à jour les coordonnées en respectant les timings de la puce vidéo (et faire en sorte que ça ne clignote pas, par exemple).

Les SpritePages ajoute un niveau de correspondance entre les identifiants logiques des graphismes (ceux que le développeur utilise, "c'est sur la page 3, deuxième ligne, première colonne) et les emplacement "physiques" dans la mémoire vidéo (42eme bloc sur 256 en mémoire). Bref, le contenu de la SpriteRam est généralement un joyeux chaos résultant des créations et supressions de blocs alors que chaque SpritePage est organisée exactement comme l'utilisateur le décide.

|

< le brol de la SpriteRam | la SpritePage, aussi organisé que vous >

Code:
class Hero : private UsingSprites {
  NOCOPY(Hero);
  const SpritePage *page;
  unsigned x, y;
  oamno_t oam;
public:
  Hero(const SpritePage *pg) : page(pg), x(128), y(96),
         oam((oamno_t) gResources->allocate(RES_OAM))
  {
    page->setOAM((blockno_t)2, sprites);

  }

  void move(int dx, int dy) {
    x += dx; y += dy;
    iprintf("(%u,%u)", x, y);
    page->changeOAM(sprites + oam, x, y, (blockno_t) 4);
  }
};
Notre petite classe 'Hero' montre tout ce qu'il faut pour avoir un objet mobile:
- se souvenir des coordonnées (le bon mot pour parler de la position à l'écran) actuelles;
- une SpritePage qui sait comment mettre à jour les entrées de la "MAO" (ou Mémoire des Attributs d'Objets -- Object Attribute Memory -- le terme de Nintendo pour parler des blocs de contrôle des sprites);
- avoir réservé une entrée de MAO et en retenir le numéro.

En disant que notre classe dérive de "UsingSprites", on obtient l'accès au tableau des MAOs (le tableau 'sprites') que le moteur de jeu va utiliser pendant la fonction sync(), mais aussi au gestionnaire de ressources (`gResources`) qui nous donne notre numéro de MAO. Du coup, la SpritePage a tout ce qu'il lui faut pour faire son travail.

Code:
void setactive() {
   SpriteRam myRam(WIDGETS_CHARSET(512));
   SpriteSet mySet(&myRam, BG_PALETTE);

   mySet.Load("efs:/bg.spr");
/*+*/ sprSet.Load("efs:/hero.spr");

  // rest of MetaWindow::setActive() as defined in previous tutorial
/*+*/ hero = new Hero(sprSet.getpage(PAGE0));
}
Charger les graphismes des sprites en mémoire vidéo ressemble très fort à ce qu'on a déjà fait pour les décors dans le tutoriel précédent. Sauf que cette fois, il faut s'assurer que les objets "SpriteSet" et "SpriteRam" resteront 'en vie' au moins aussi longtemps que notre Hero (qui les utilisera à travers la SpritePage). Du coup, on en fait des membres de la MetaWindow, plutôt que des variables temporaires. Ça signifie aussi qu'il nous faut attendre que les données soient chargées avant de pouvoir créer notre Héro.

Puis il n'y a plus qu'à tester l'état de la croix directionnelle (et les transmettre par des appels à la fonction 'move()') pour pouvoir déplacer le personnage en réaction aux mouvements imprimés par le joueur. Bon, ne vous attendez pas à quelque-chose de fluide ici: j'utilise toujours l'interface utilisateur prévue pour les éditeurs du projet "GEDS" qui ne produit un 'évèment DPAD' que lorsqu'on appuie sur une nouvelle direction. On arrangera ça la semaine prochaine avec les Animators.

Code:
if (keys & KEY_UP) {
  hero->move(0, -4);
}
if (keys & KEY_DOWN) {
  hero->move(0, 4);
}
if (keys & KEY_LEFT) {
  hero->move(-4, 0);
}
if (keys & KEY_RIGHT) {
  hero->move(4, 0);
}
Les détails sont sur github, comme toujours.

Miniatures attachées
Cliquez sur l'image pour la voir en taille réelle

Nom : SpritePage.png
Affichages : 1874
Taille : 2,8 Ko
ID : 1547  

Dernière modification par PypeBros ; 25/03/2018 à 22h36. Motif: oups.
PypeBros est déconnecté   Réponse avec citation
Vieux 04/04/2018, 22h52   #4
PypeBros
Membre confirmé
 
Date d'inscription: 15/10/2007
Messages: 69
Par défaut

Bon, il est temps de vous montrer ce que libgeds peut faire pour ceux qui n'ont pas le C++ dans le sang. Charger des fichiers, définir des animations, positionner les ennemis dans le niveau, tout ça peut être fait à partir de commandes dans des scripts texte lus et transformés en objets C++ par la bibliothèque.

Je vais commencer par un sous-script qui décrit le comportement d'un personnage. On va commencer très simple, avec une seule animation, et toujours le même comportement: ne pas bouger. Voici donc "xdad.cmd"
Code:
# this is xdad.cmd behaviour description
anim1 0 {
  spr0 4
  delay 3
  spr0 5
  delay 3
  spr0 6
  delay 3
  spr0 7
  delay 3
  spr0 8
  delay 3
  spr0 9
  delay 3
  spr0 a
  delay 3
  spr0 b
  delay 3
  loop
}

state0 :anim1

end

Le bloc 'anim<numéro-d-animation>' donne utilise une page du SpriteSet et construit une animation en indiquant les images à utiliser (dans cette page) et combien de temps attendre entre chaque deux images. On peut ensuite l'attacher à un état, c'est à dire une unité élémentaire de comportement . Bon, évidemment, pour l'instant, avec un seul état, il ne se passera pas grand-chose. C'est un peu notre 'hello world'.

Un fichier comme xdad.cmd, on en créera un par type de personnage (imaginez "goomba.cmd", "koopa.cmd", etc.) Maintenant, il va nous falloir un script qui décrit un niveau. Ce sera demo.cmd, pour nous.
Code:
#demo.cmd, resource management
print "loading tileset"
bg0.load "../bg.spr"
print "loading sprites"
spr.load "../hero.spr":1
On y retrouve des commandes gérant le chargement des fichiers de données qui remplace les SpriteSet::Load().

Code:
#demo.cmd, use 'character' description
input "xdad.cmd"
import state 0
On y retrouve aussi évidemment des commandes pour réutiliser les fichiers comme xdad.cmd, évidemment. la commande 'import state 0' mérite quelques mots d'explication supplémentaires. Pour un vrai personnage de jeu vidéo, il y aura bien sûr de nombreux états... On peut en compter au moins 21 par direction dans Super Mario world, par exemple.

Pourtant, seuls un ou deux de ces états sont nécessaires pour décrire le niveau (commencer tourné vers la droite ou vers la gauche, par exemple, ou éventuellement par une glissade). Geds propose donc un jeu d'identifiants pour l'ensemble des états d'un personnage (tous les états d'un goomba, par exemple) qui sera utilisé dans xdad.cmd, et l'ensemble des états utilisables pour créer des nouveaux objets (un goomba, mario, un koopa rouge ou un koopa vert), qui sera utilisé dans demo.cmd.
Une fois le traitement xdad.cmd terminé, la commande 'import state 0' indique de prendre le premier état de l'ensemble de xdad et d'en faire l'état numéro 0 pour demo.cmd.

Code:
# creating gobs
gob0 :state0 (128, 100)
end
Enfin, on peut constuire un nouveau personnage (le Game object) avec cet état, en donnant les coordonnée du personnage à l'écran.

Comme on le voit sur github, on laisse tomber la classe Hero: il s'agit maintenant d'un GameObject construit et contrôlé directement par la bibliothèque suite au commandes du script.

La classe 'MetaWindow', point de départ de notre démo, a pas mal changé aussi.
Code:
#include <GameWindow.hpp>
class MetaWindow : public DownWindow {
  NOCOPY(MetaWindow);
  Window *active;
  InputReader* reader;
  LoadingWindow loadwin;
  GameWindow gamewin;
public:
  MetaWindow(): active(0),
   reader(0),
   loadwin(&reader),    gamewin(this, &reader, &loadwin)  {
     ntxm9 = new NTXM9(); // for the sound, remember ?
     active = &gamewin;
   }
C'est la classe GameWindow de libgeds qui va créer, charger et traiter les scripts, et assurer la gestion de tous les objets qui en découlent, les animations, les personnages, etc. Sa compagne, LoadingWindow sert lors des changement de niveaux (on y reviendra). Pas grand chose à dire sur le constructeur puisqu'à ce moment-là, les fichiers intégrés à la ROM ne sont pas encore disponibles. L'initialisation, la vraie, se passera dans 'setactive()'.

Code:
   void setactive() {
     FileDataReader fd("efs:/sndtrk.xm");
     reader = new FileReader("efs:/demo.cmd");
     ntxm9->stop();
     u16 err = ntxm9->load(&fd);
     ge.setWindow(active);
     restore(); // just to have the tileset shown again.
     if (err!=0) iprintf("ntxm says %x\n",err);
   }
Par le simple fait de demander au moteur de jeu de passer le contrôle à GameWindow, le chargement commence automatiquement à travers le 'FileReader' (lecteur de fichiers, donc) qu'on vient de construire.
Et c'est tout. A la prochaine fois pour rendre ses déplacement à notre petit père Noël.
PypeBros est déconnecté   Réponse avec citation
Vieux 25/04/2018, 00h05   #5
PypeBros
Membre confirmé
 
Date d'inscription: 15/10/2007
Messages: 69
Par défaut



Redonner du mouvement au personnage dans geds, ça correspond à spécifier une série de contrôleurs pour l'état choisi. Chaque contrôleur a une mission bien précise, p.ex. ici
Code:
state0 :anim1 {
   using dpad
   using momentum(x to 512)
   using momentum(y to 512)
}
dpad va lire les registres de la console et sauver les directions
momentum va utiliser les directions sauvées pour augmenter ou diminuer la vitesse horizontale ou verticale (selon qu'on a utilisé "x" ou "y")

On peut aussi passer des paramètres aux contrôleurs: c'est le cas ici du "512" qu'il faut interpréter comme un nombre de 256emes de pixels par frame (1/60eme de seconde) et qui représente la vitesse maximale autorisée sur un axe.

Le ScriptParser cherchera les classes C++ correspondantes et fait le nécessaire pour que le code qu'elles contiennent soient appelées à chaque image générée par le jeu.

Trois lignes de plus dans le script donc (et quelques modifications dans les Makefiles). C'est tout.
PypeBros est déconnecté   Réponse avec citation
Réponse

Liens sociaux

Publicité



Utilisateurs regardant la discussion actuelle : 1 (0 membre(s) et 1 invité(s))
 
Outils de la discussion
Modes d'affichage

Règles de messages
Vous ne pouvez pas créer de nouvelles discussions
Vous ne pouvez pas envoyer des réponses
Vous ne pouvez pas envoyer des pièces jointes
Vous ne pouvez pas modifier vos messages

Les balises BB sont activées : oui
Les smileys sont activés : oui
La balise [IMG] est activée : oui
Le code HTML peut être employé : non
Navigation rapide

Discussions similaires
Discussion Auteur Forum Réponses Dernier message
NDS Installation et fonctions du Supercard DS One heaveN. Articles et Tutos 218 12/04/2011 09h47
[Divers] PMP (Portable Game & Media Player) : La nouvelle console cheap! vgiant Articles 46 07/01/2010 13h46
Concours de quéquettes rhétoriques dolarcles Récréation 20 19/12/2009 19h40
Problème avec les jeux NTSC sur ma Playstation Lord Raptor Discussions Sur Les Autres Consoles 2 26/08/2007 20h11
Tuto sur les problèmes Wifi avec les homebrews olive_69 [NDS] Divers 10 12/03/2007 23h27


Fuseau horaire GMT +2. Il est actuellement 10h42.


Édité par : vBulletin® version 3.7.2
Copyright ©2000 - 2023, Jelsoft Enterprises Ltd. Tous droits réservés.
Version française #16 par l'association vBulletin francophone
Design par Ass-Itch, DJP et Dr.Vince