15 juin 2007

Exploration de XNA : affichage de sprites

Ce billet est le troisième d'une série consacrée à la plateforme XNA de Microsoft. Au cours de cette série, nous étudierons plusieurs aspects du développement de jeux vidéo, tant au niveau de l'architecture qu'au niveau de la programmation elle même - avec au final un but : vous permettre de vous lancer à la poursuite de vos rêves. Enfin, si programmer un jeu est votre rêve.

Le code correspondant à cet article est disponible ici.

xna_logo.png [Télécharger Visual C# 2005 Express Edition] [Télécharger XNA Game Studio 1.0 Express Edition] [Télécharger le Service Pack 1 pour VC# 2005 EE] [TorqueX, de GarageGames.com] [XNA Resources] [Forum MSDN XNA Game Studio Express] [GameDev.Net - Plenty of 1s and 0s]

Un exemple simple de simplicité

Cette fois ci, on attaque le vif du sujet - en commençant par faire simple : nous allons afficher quelques sprite dans une fenêtre XNA. Avant tout, quelques définitions s'imposent.

Historiquement, un sprite est une image 2D de petite taille représentant un personnage ou un autre petit élément graphique, tel qu'un missile. La plupart du temps, ces images regroupée dans ce qu'on appelle une sprite sheet, qui contient le même élément vu sous différents angles ou à un stade différent de son évolution dans le temps. C'est de cette manière que les jeux comme Xenon II[1] gèrent les animations. Pour ce premier exemple, nous allons nous limiter à des sprites statiques (c'est à dire sans animation attachée) afin de présenter le principe du rendu de sprites avec XNA.

Bien évidemment, cette notion de sprite est fortement attachée à la notion de display 2D. En sortant DirectX 8 il y a quelques année, Microsoft a supprimé le support de la 2D pour ne fournir que le support de la 3D. Bien entendu, qui peut le plus peut le moins, et il est tout a fait possible de simuler un écran purement 2D même en utilisant un toolkit 3D tel que les versions récentes de DirectX - y compris DirectX 9, sur lequel est basé le framework XNA. Le problème principale est la gestion des coordonnée. Une coordonnée 3D - l'entrée principale des algorithmes de transformation de DirectX 8+ - est exprimée dans l'espace du jeu, et non pas dans l'espace de l'écran. Il faut donc normalement transformer ces coordonnées (via une matrice de projection) pour obtenir les coordonnées de chaque point sur l'écran. Il est évident que cette technique est impraticable pour une gestion efficace des sprites - imaginer vous devoir calculer les coordonnées 3D en fonction d'une matrice 4x4 de projection (qui n'est pas une fonction linéaire) pour obtenir l'effet voulu sur l'écran. Par bonheur, DirectX supporte aussi un autre mode d'entrée : il est possible de l'alimenter avec des coordonnées pré-transformées, c'est à dire dans l'esapce 2D de l'écran. Tout à coup, tout devient plus simple.

Il n'en reste pas moins une différence de taille: lorsque l'API DirectX permettait le rendu 2D (via la sous-API DirectDraw), il suffisait de dire quelle zone de l'écran était couverte par quelle image et le tour était joué. En utilisant une API 3D, les choses se compliquent un peu. En effet, même si on peut fournir à DirectX des coordonnées pré-transformées, celui-ci n'en demeure pas moins limité à un seul type de primitive au niveau du rendu: les triangles, sur lesquels on peut placer une texture. Pour effectuer le rendu d'un sprite, il faut donc initialiser le moteur de rendu avec deux triangles rectangles qui partagent la même hypoténuse, et donner à DirectX les informations relatives à texture (l'image qui contient le sprite) utilisée dont il aura besoin.

On le voit, ce n'est pas tout à fait simple. Fort heureusement, les personnes qui ont conçu XNA l'ont voulu simple d'utilisation. Ils ont donc caché l'ensemble des traitements nécessaires au rendu d'un sprite dans une classe particulière (Microsoft.Xna.Framework.Graphics.SpriteBatch), et ils ont laissé au programmeur le soin de jouer avec la texture contenant le sprite.

Et concrètement, comment ça se passe ?

Pour effectuer le rendu d'un sprite, nous avons donc besoin de deux choses: une texture en 2 dimension (l'image du sprite), et un objet du type SpriteBatch qui effectuera toutes les opérations fastidieuses d'initialisation du rendu pour nous.

Creation et utilisation d'un SpriteBatch

En premier lieu, il faut créer cet objet. Le paramètre du constructeur de SpriteBatch est une instance valide de GraphicsDevice. La méthode Game1.Initialize() est parfaite pour ça, puisque lorsqu'on entre dans cette méthode, une instance de GraphicsDevice a déja été créée (elle est créée entre le moment ou on sort du constructeur de Game1 et le moment ou on entre dans cette méthode. Par conséquent, il est impossible de créer le SpriteBatch dans le constructeur de l'application).

protected override void Initialize()
{
  spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
  base.Initialize();
}

Lorsque nous devons ensuite effectuer le rendu du sprite, rien de plus simple:

protected override void Draw(GameTime gameTime)
{
  // clear the back buffer
  graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(); spriteBatch.Draw(spriteTexture, spritePosition, Color.White); spriteBatch.End();
base.Draw(gameTime); }

la méthode SpriteBatch.Draw(), dans sa forme la plus simple, prends trois paramètres:

  • la texture 2D contenant l'image du sprite (du type Texture2D)
  • la position 2D à laquelle il faut dessiner le sprite (Vector2)
  • une couleur avec laquelle on va moduler les couleurs du sprite (Color; pour ne pas effectuer de modulation, il faut préciser la couleur blanche, comme dans l'exemple ci-dessus).

Avant de pouvoir appeler cette méthode SpriteBatch.Draw(), il faut informer l'instance de SpriteBatch qu'une ou plusieurs commandes de dessin vont être envoyée. Cela se fait avec un appel à SpriteBatch.Begin(). En interne, l'objet va préparer les structures et les buffers qui seront utilisée pour stocker les informations de rendu 3D. Une fois que les commandes de dessin ont été envoyées, l'appel à la méthode SpriteBatch.End() sérialise les informations de rendu vers la carte graphique. Si l'appel à Begin() n'est pas effectué ou échoue, les appels subséquent à Draw() vont générer une exception. Si l'appel @ End() n'est pas fait, le dessin ne se fera pas - et il est probable qu'un nouvel appel à Begin() génère une exception.

Création du sprite proprement dit

Bien, nous savons comment effectuer le rendu, il ne nous reste plus qu'à créer notre sprite. Figurez vous que c'est la aussi d'un simplicité enfantine, grâce au Content Manager (CM) de XNA Game Studio.

Dans la course à la simplicité, le CM est d'une aide précieuse : grâce à lui, nul besoin de connaitre la structure d'un fichier pour pouvoir le charger. On peut donc charger sans problème des fichiers complexes (pour mon exemple, j'ai choisi un fichier PNG car ce type de fichier gère la transparence) en une unique ligne de code. Encore faut-il importer correctement l'image que l'on souhaite manipuler dans le CM, et cela se fait très aisément - en fait, on ajoute des fichiers au CM de la même manière qu'on ajoute des fichiers au projet: project -> add existing item ..., sélection du fichier PNG voulu (on peut visualiser plus aisément les fichiers supportés par le CM en sélectionnant le filtre de fichiers correspondant dans la boite de dialogue d'ouverture de fichiers), et voilà.

Une fois ajouté au projet, on peut vérifier les propriétés de la nouvelle ressource, qui sont au nombre de 8 (dont une non modifiable)

  • Asset Name : c'est par le biais de ce nom que le fichier sera chargé dans l'application; il s'agit d'une chaine de caractère, à vous de préciser laquelle (par défaut, c'est le titre du fichier, c'est à dire le nom de fichier sans le chemin ni l'extension).
  • Build Action : laissez à Content
  • Content Importer : pour une texture 2D, le content importer doit être du type Texture
  • Content Processor : pour une texture 2D, le content processor doit être du type Texture2D
  • Copy to output directory : il doit être laissé à do not copy dans ce cas; voir la raison ci-dessous.
  • File name : le nom du fichier
  • File path : le chemin absolu du fichier (non modifiable)
  • XNA Framework Content : True si le fichier doit être traité par le Content Manager de XNA (ce qui est notre cas).

Un mot d'explication s'impose pour décrire le Content Manager. Les fichiers ajoutés au CM sont traités lors de la compilation afin d'accélérer et de simplifier leur chargement au runtime. Lorsque XNA Framework Content est à True, XNA Game Studio applique le Content Processor au fichier afin de le rendre compatible avec le Content Importer correspondant. Dans ce process, les fichiers résultants sont copiés dans le répertoire de destination sous une nouvelle forme (pour un fichier PNG, on obtient un fichier XNB). Si le Game Studio ne traite pas la ressource, il faudra alors lui demander explicitement de la copier dans le répertoire de destination.

Une fois traitée, le runtime peut charger la texture 2D. Cette opération prend place dans la méthode Game1.LoadGraphicsContent(), lorsque loadAllContent est à true. grâce à l'instance du ContentManager créée dans le constructeur de Game1, une seule ligne suffit pour effectuer le chargement:

protected override void LoadGraphicsContent(bool loadAllContent)
{
  if (loadAllContent == true)
  {
    spriteTexture = content.Load<Texture2D>("your_resource_name");
  }
// TODO: Load any ResourceManagementMode.Manual content }

Attention au nom de la ressource: si elle est placée dans un sous répertoire de l'application (j'ai l'habitude de mettre mes fichiers ressource dans le sous-répertoire Content, d'autres peuvent préférer Data), le nom de la ressource est formé de son chemin relatif et du asset name choisi. Par exemple : "Content\\boy".

On récapitule le code source - avec les différentes déclarations[2]:

public class Game1 : Microsoft.Xna.Framework.Game
{
  GraphicsDeviceManager graphics;
  ContentManager content;
  SpriteBatch spriteBatch = null;
  Texture2D spriteTexture = null;
  Vector2 spritePosition;
public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services);
// init the graphic driver properties graphics.PreferredBackBufferWidth = 320; graphics.PreferredBackBufferHeight = 200;
// init the sprite position spritePosition.X = 0; spritePosition.Y = 0; }
protected override void Initialize() { // the goal of the sprite batch is to display the sprites spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
base.Initialize(); }
protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent == true) { // load the sprite textures spriteTexture = content.Load<Texture2D>("Content\\boy"); } }
protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } }
protected override void Update(GameTime gameTime) { // Allows the default game to exit on Xbox 360 and Windows if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();
// nothing to update in this sample
base.Update(gameTime); }
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(spriteTexture, spritePosition, Color.White); spriteBatch.End();
base.Draw(gameTime); } }

Le résultat de ces quelques lignes de code est édifiant (tout en restant somme toute très modeste :))

Exemple avec un seul sprite
XNA, rendu d'un sprite

Example avec deux sprites
XNA, rendu d'un sprite

Bien évidemment, on peut complexifier à loisir ce code. Pour ma part, j'ai choisi d'encapsuler la gestion d'un sprite dans une classe Sprite, afin d'en faciliter l'utilisation. Vous pouvez télécharger le code correspondant à cet article et l'étudier à loisir - il n'y a rien de compliqué, alors amusez vous !

Le code correspondant à cet article est disponible ici.

Notes

[1] en 2000, Bitmap Brother a sorti une suite gratuite à ce jeu mythique; vous pouvez la télécharger ici

[2] le sprite présenté ici a été créé par Danc

Commentaires

1. Le mardi, avril 22 2008, 10:04 par Choupette

Salut ! Merci pour les éclaircissements. Par contre ce n'est pas possible de télécharger le code. Pourrais-tu y remédier ? Mici !

2. Le mardi, avril 22 2008, 10:41 par Emmanuel Deloget

Hum. Je ne comprends pas trop ce qui se passe : je n'ai aucun problème pour télécharger le code. Peux-tu ré-essayer ?

3. Le jeudi, novembre 5 2009, 18:14 par Thomas

Bravo pour ton post, c'est limpide. Mais j'ai une question, je suis sur un projet où je dois charger une image du web pour affichage sur XBOx 360. Est-ce qu'il y a un moyen de faire ça ? J'ai l'impression qu'il faut toujours compiler les images avec l'application pour que ce soit pris en compte, mais qu'on ne peut pas avoir l'image provenant d'une source externe à la volée. Je me trompe ? Il y a une astuce ? Merci !!!

4. Le samedi, septembre 1 2012, 14:07 par Papayo

Bonjour Emmanuel,
Même remarque que Choupette, impossible de télécharger le Code Source :-(

Merci d'avance

5. Le lundi, septembre 3 2012, 18:14 par Emmanuel Deloget

@Papayo : en même temps, on parle de code qui a 4 ans, et qui ne concerne que XNA 1 ; XNA a suffisamment évolué depuis pour que ce code ne soit plus vraiment à jour. Je ne vais pas essayer de le retrouver parmi mes innombrables CD ou DVD de sauvegarde - le code n'est pas assez significatif pour que je me donne cette peine. Les articles eux même sont obsolètes (quasiment la série complète, même si les concepts expliqués restent corrects).

Je suis désolé de cet état de fait. Si un jour, je tombe dessus (en cherchant autre chose de plus intéressant), je mettrais le billet à jour avec un nouveau lien.

6. Le vendredi, septembre 7 2012, 23:50 par Papayo

Merci Emmanuel pour ta réponse. Je comprends... 4 ans après, cela n'est pas évident !

A bientôt.

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet