Exploration de XNA : affichage de sprites | 3 vote(s)
Par Emmanuel Deloget, vendredi 15 juin 2007 à 23:00 :: Exploration de XNA :: permalien #80
Fichier(s) attaché(s) :
Tags: C#, game programming, sprites, xna
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.
[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
![]()
Example avec deux sprites
![]()
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.
Commentaires
1. Le mardi 22 avril 2008 à 10:04, par Choupette
2. Le mardi 22 avril 2008 à 10:41, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire