XNA Framework : Les bases de l'affichage 2D / Création d'un GameComponent

XNA Framework : Les bases de l'affichage 2D / Création d'un GameComponent

Préambule

Ce tutoriel n'est valable que pour la beta 1 de XNA Game Studio Express. Réécriture en cours pour couvrir les changements apportés par la version 1.0
Mes sources sont quant à elles mises à jour et couvrent déjà ces changements.

Introduction sur le XNA Framework

XNA Framework est une plate-forme permettant de développer des jeux en C# sur PC et XBOX 360, le tout avec un pourcentage infime de changements dans le code (estimé à 5-10% selon l'équipe de développement, à ce stade de l'évolution du Framework).

XNA Framework se base sur le .Net Compact Framework 2.0, et pour le moment n'est supporté que sur Windows XP malheureusement. Mais nous n'en sommes qu'à une beta 1 de XNA Game Studio Express.

Effectuer un rendu 2D

Ce petit tutoriel n'aura pas pour but d'explorer toutes les facettes de l'affichage 2D, mais d'expliquer comment effectuer simplement la gestion et le rendu de plusieurs sprites à travers un mini-projet : un « Scrolling Background ».

Nous allons donc développer un petit composant (GameComponent) qui permettra l'affichage d'un fond multi-plan (un sprite = un plan), chaque plan ayant sa propre vitesse de défilement. Ceci afin de créer un effet de profondeur.

Voici les classes du XNA Framework que nous emploieront :

  • Game : La classe principale d'un jeu. Elle doit hériter de la classe Game. Cette classe contient 3 méthodes qui nous intéressent particulièrement
Nom Description
protected void Draw() C'est dans cette méthode que seront placées toutes les instructions de rendu d'un jeu
protected void Update() C'est dans cette méthode que sera gérée toute la logique d'un jeu (Mise à jour de la position des objets, ...)
protected virtual void OnStarting() Cette méthode est appelée au démarrage du jeu, juste avant l'affichage de la première image à l'écran

Elle contient également une propriété GameComponents, qui est une collection de GameComponent appartenant au jeu.

  • GameComponent : C'est la classe de base pour tous les composants d'un jeu. Cette classe contient 2 méthodes publiques qui nous intéressent : Draw et Update, ayant la même utilité que celles de la classe Game. Cette classe contient aussi une propriété Game, permettant d'accéder à la classe Game associée à notre GameComponent.
  • GraphicsComponent : C'est le composant qui gère la configuration et la gestion de notre device graphique
  • GraphicsDevice : Le device graphique
  • Vector2D : Un vecteur a 2 composantes
  • Texture2D : son nom est assez explicite : Une texture 2D
  • SpriteBatch : C'est la classe qui permet le rendu de nos textures

Le squelette de notre classe principale

Comme dit plus haut, la classe principale de tout jeu doit hériter de la classe Game. Il faut ensuite lui attribuer une instance de la classe GraphicsComponent qui s'occupe, entre autres, de la gestion de notre carte graphique.

Note : contrairement au Managed DirectX, il n'y a pas besoin de configurer notre device graphique. On instancie la classe GraphicsComponent, et le Framework s'occupe du reste.

Une fois le GraphicsComponent instancié, il suffit de l'ajouter à la collection GameComponents de notre classe héritant de Game et... C'est tout !

private GraphicsComponent graphics; // Le component qui gère la partie graphique 
public TestScrollingBackground() 
{ 
   InitializeComponent(); 
}
private void InitializeComponent() 
{ 
   this.graphics = new GraphicsComponent (); 
   this.graphics.BackBufferWidth = 800; // Largeur du BackBuffer 
   this.graphics.BackBufferHeight = 600; // Hauteur du BackBuffer 
   this.GameComponents.Add(graphics); 
}

La méthode OnStarting de la classe Game est appelée au démarrage de jeu, avant que la première image ne soit dessinée à l'écran. C'est dans cette méthode que nous allons appeler le chargement de nos ressources. Nous en profiterons aussi pour nous inscrire aux événements DeviceReset et DeviceCreated de notre GraphicsComponent.

// Cette méthode s'exécute quand le jeu 
// est initialisé, mais avant l'affichage de la première image 
protected override void OnStarting()
{
   // Se déclenche lors de la création d'un nouveau Device 
   this.graphics.DeviceCreated += new EventHandler (graphics_DeviceCreated);
   // Se déclenche lorsqu'on switch en mode Plein écran/fenétré, ou lorsqu'on réduit la fenêtre 
   this.graphics.DeviceReset += new EventHandler (graphics_DeviceReset);
   LoadResources();
}
private void graphics_DeviceReset( object sender, EventArgs e)
{
   LoadResources();
}
private void graphics_DeviceCreated( object sender, EventArgs e)
{
   LoadResources();
}
// Charge les ressources utilisées dans cette application
// (Textures, ...) 
private void LoadResources()
{
} 

Et passons finalement aux méthodes Update et Draw, commentées ci-dessous.

// On met à jour toutes les données 
// Cette méthode est appelée avant la méthode Draw() 
protected override void Update()
{
   //Mise à jour des GameComponents 
   UpdateComponents();
}
// Cette méthode est appelée après la méthode Update() 
// et dessine nos textures à l'écran 
protected override void Draw()
{
    //On s'assure qu'on a un device graphique valide 
    if (!graphics.EnsureDevice())
       return ;
    // On repeint l'écran en bleu 
    this.graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
    this.graphics.GraphicsDevice.BeginScene();
    //Rendu des composants 
    DrawComponents();

    graphics.GraphicsDevice.EndScene();
    graphics.GraphicsDevice.Present();
}

Voici le squelette de notre « jeu ». Nous le compléterons à la fin de notre article afin d'y intégrer notre Background.

Un peu plus dans le vif du sujet

Développement de notre GameComponent

Pour réaliser notre Scrolling Background, nous allons créer 2 classes :

  • La classe ScrollingBackgroundComponent, qui héritera de GameComponent et qui s'occupera de gérer et d'ordonner l'affichage de nos BackgroundSprites .
  • La classe BackgroundSprite, qui est en fait un sprite appartenant à notre Background. Elle contiendra les coordonnées du sprite, sa texture et sa vitesse de défilement.

Bien que cette classe n'héritera pas de la classe GameComponent, nous l'agrémenterons des méthodes Draw et Update afin de garder une certaine logique dans le nommage / utilité de nos méthodes. Nous ajouterons aussi une petite énumération, qui permettra de définir dans quelle direction se fait le scrolling (Horizontal ou Vertical).

// Direction du scroll 
public enum ScrollDirection
{
    // Scrolling horizontal 
    Horizontal = 0,
    // Scrolling vertical 
    Vertical
}

La classe ScrollingBackgroundComponent

Comme dit plus haut, cette classe va nous permettre d'afficher plusieurs sprites à la fois (représentés par la classe BackgroundSprite, décrite plus bas), chaque sprite ayant un coefficient de défilement différent, afin de créer un effet de profondeur (plus les sprites sont éloignés, plus ils défilent lentement).

Pour pouvoir faire du rendu graphique depuis notre classe, nous devons récupérer le device graphique défini dans la classe principale. C'est une chose très facile à faire depuis un GameComponent. Pour cela, on peut se servir de sa propriété GameComponent.Game, qui nous permet d'accéder à l'instance de la classe Game associée à notre GameGomponent de cette façon :

graphicsDevice = this.Game.GameServices.GetService<IGraphicsDeviceService>().GraphicsDevice;

Voici le squelette de base d'un GameComponent graphique :

public class MyGameComponent : GameComponent 
{ 
    private GraphicsDevice graphicsDevice;

    public MyGameComponent()
    {
        InitializeComponent();
    }
    public override void Start() 
    { 
        this.graphicsDevice = this.Game.GameServices.GetService<IGraphicsDeviceService>().GraphicsDevice; 
    } 

    public override void Update() 
    { 
    }

    public override void Draw() 
    { 
    } 
} 

Complétons cette classe pour réaliser notre ScrollingBackground. Notre Background est composé de plusieurs BackgroundSprites. Pour respecter l'ordre d'affichage de nos sprites, nous allons utiliser la classe générique SortedDictionary<int, BackgroundSprite>.

De quelles autres variables aurons-nous besoin ? La direction du scroll, la vitesse de défilement et une instance de la classe SpriteBatch pour le rendu.

Ce qui nous donne :

private SortedDictionary<int, BackgroundSprite> backgroundSprites;
private IDictionary<int, BackgroundSprite> syncLock;
private SpriteBatch spriteBatch;
private GraphicsDevice graphicsDevice;
private ScrollDirection scrollDirection;
private int scrollSpeed;
// Direction du scroll 
public ScrollDirection ScrollDirection
{
    get { return this.scrollDirection; }
    set { this.scrollDirection = value ; }
}
// Vitesse de défilement 
public int ScrollSpeed
{
    get { return this.scrollSpeed; }
    set { this.scrollSpeed = value ; }
} 

Passons à l'initialisation de notre GameComponent. Nous avons vu un peu plus haut comment récupérer notre Device graphique. Complétons l'initialisation avec les nouvelles variables ajoutées :

public ScrollingBackground()
{
   this.backgroundSprites = new SortedDictionary<int, BackgroundSprite>();
   this.syncLock = ( IDictionary<int, BackgroundSprite>) this.backgroundSprites;
}
public override void Start()
{
    this.graphicsDevice = this.Game.GameServices.GetService().GraphicsDevice;
    this.spriteBatch = new SpriteBatch( this.graphicsDevice);
    this.scrollDirection = ScrollDirection.Horizontal;
    this.scrollSpeed = 1;
}

La méthode AddBackground ajoute un sprite à notre background. Elle prend en paramètres l'ordre d'affichage (zorder) du sprite, le chemin de sa texture, ses coordonnées sous forme de 2 entiers et son coefficient de défilement. Cette méthode crée une instance de BackgroundSprite et l'ajoute à notre SortedDictionnary.

A noter que si on ajoute un sprite avec un ordre d'affichage existant déjà dans notre collection de sprites, celui-ci est ignoré.

public int AddBackground(int textureZOrder, string texturePath, int xpos, int ypos, int speedCoeff)
{
    if (!this.backgroundSprites.ContainsKey(textureZOrder))   
    {
        BackgroundSprite sprite = BackgroundSprite.CreateBackgroundSprite(this.graphicsDevice, texturePath, xpos, ypos, speedCoeff);
        if (sprite != null)
        {
            lock (syncLock)
            {
                this.backgroundSprites.Add(textureZOrder, sprite);
            }
        }
    }
    return this.backgroundSprites.Count;
}

Terminons avec les méthodes Update et Draw. Celles-ci se contentent respectivement d'ordonner tous nos sprites de mettre à jour leurs coordonnées (en tenant compte de la direction du scroll et de la vitesse) et de s'afficher.

Cependant, dans la méthode Draw, nous devons préalablement appeler la méthode SpriteBatch.Begin(). Cet appel prépare le device graphique à effectuer le rendu des sprites. Nous lui fournissons le paramètre SpriteBlendMode.AlphaBlend pour lui spécifier qu'il faut activer l'AlphaBlending, ce qui signifie que l'on tient compte de la transparence des textures.

Une fois le rendu de nos sprites terminé, nous appellons la méthode SpriteBatch.End() qui rend au device son état initial.

public override void Update()
{
    foreach (BackgroundSprite sprite in this.backgroundSprites.Values)
        sprite.Update(this.scrollDirection, this.scrollSpeed);
}
public override void Draw()
{
    this.spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
    foreach (BackgroundSprite sprite in this.backgroundSprites.Values)
        sprite.Draw(this.spriteBatch, this.scrollDirection);
    this.spriteBatch.End();
}

La classe BackgroundSprite

Je disais un peu plus haut que cette classe allait contenir les coordonnées du sprite, sa texture, et sa vitesse de défilement (ou plutôt, son coefficient de défilement). Voici les champs et propriétés déclarés :

private Vector2 position;
private Texture2D texture;
private int speedCoeff;
// La texture du BackgroundSprite 
public Texture2D Texture
{
    get { return this.texture; }
    set { this.texture = value; }
}
// Le coefficient de défilement du sprite 
public int SpeedCoeff
{
    get { return this.speedCoeff; }
    set { this.speedCoeff = value; }
}
// La coordonnée X su sprite à l'écran 
public int XPos
{
    get { return ( int ) this.position.X; }
    set { this.position.X = value; }
}
// La coordonnée Y du sprite a l'écran 
public int YPos
{
    get { return ( int ) this.position.Y; }
    set { this.position.Y = value; }
}

Nous n'allons pas exposer de constructeur public pour notre classe, mais plutôt créer une méthode statique qui se chargera d'instancier le BackgroundSprite à notre place. Cette méthode prendra comme paramètres les coordonnées du sprite, le chemin vers la texture que nous lui appliquerons, son coefficient de défilement, ainsi que le device graphique qui nous permettra de dessiner ce sprite. Ce paramètre est nécessaire, car il est requis à la création de la texture.

public static BackgroundSprite CreateBackgroundSprite( GraphicsDevice graphicsDevice, string spritePath, int xpos, int ypos, int speedCoeff) 
{ 
    if (File.Exists(spritePath)) 
    { 
        return new BackgroundSprite (graphicsDevice, spritePath, xpos, ypos, speedCoeff); 
    } 
    return null ; 
}
private BackgroundSprite(GraphicsDevice graphicsDevice, string spritePath, int xpos, int ypos, int speedCoeff)
{
    this.speedCoeff = speedCoeff;
    this.position = new Vector2(xpos, ypos);
    this.texture = Texture2D.FromFile(graphicsDevice, spritePath);
}

Et finalement, nous allons attaquer les 2 méthodes les plus importantes, à savoir Update et Draw.
La méthode Update va nous permettre de faire avancer ou reculer nos sprites. Rien de très compliqué en soit. Nous allons lui fournir 2 paramètres, un premier qui indiquera la direction du défilement (horizontale ou verticale) et un second pour définir la vitesse de défilement. Cette vitesse sera multipliée par le coefficient de défilement du sprite.

public void Update( ScrollDirection scrollDirection, int scrollSpeed) 
{
    switch (scrollDirection)
   {
       case ScrollDirection.Vertical:
          this.YPos += this.speedCoeff * scrollSpeed;
          if (Math.Abs( this.YPos) >= this.texture.Height) this.YPos = 0;
          break ;
       case ScrollDirection.Horizontal:
          this.XPos -= this.speedCoeff * scrollSpeed;
          if (Math.Abs( this.XPos) >= this.texture.Width) this.XPos = 0;
          break ;
      }
}

La méthode Draw va quant à elle simplement dessiner notre sprite de façon a ce qu'il soit répété en continu, de gauche a droite ou de haut en bas. Nous lui passerons en paramètres le SpriteBatch de notre GameComponent qui s'occupera du rendu des sprites, et la direction du scroll, afin de déterminer le sens dans lequel effectuer le rendu.

public void Draw(SpriteBatch spriteBatch, ScrollDirection scrollDirection)
{
    if (scrollDirection == ScrollDirection.Horizontal)
   {   
      spriteBatch.Draw(this.texture, new Vector2(this.XPos - this.Texture.Width, this.YPos), Color.White);
      spriteBatch.Draw(this.texture, new Vector2(this.XPos, this.YPos), Color.White);
      spriteBatch.Draw(this.texture, new Vector2(this.XPos + this.Texture.Width, this.YPos), Color.White);
   }
    else
    {
      spriteBatch.Draw(this.texture, new Vector2(this.XPos, this.YPos - this.texture.Height), Color.White);
      spriteBatch.Draw(this.texture, new Vector2(this.XPos, this.YPos), Color.White);
      spriteBatch.Draw(this.texture, new Vector2(this.XPos, this.YPos + this.texture.Height), Color.White);
   }
} 

Intégration dans notre classe principale

Nous allons maintenant modifier notre classe principale afin d'y intégrer notre ScrollingBackground. Ceci est très simple. Ajoutons à notre classe un champ

private          ScrollingBackground background

. Il suffit d'ajouter dans la méthode InitializeComponent ces 2 lignes :

// Instanciation de notre Scrolling Background 
this.background = new ScrollingBackground(); 
// Que l'on ajoute à la collection de GameComponents 
this.GameComponents.Add(this.background);

Ensuite, utilisons la méthode LoadResources pour lui ajouter ses sprites. :

// Le ciel sera le plan le plus eloigné, et aura un coefficient de défilement de 2 
this.background.AddBackground(0, @"ImagesSky.png" , 0, 0, 2); 
// Les arbres seront dessinés après le ciel, et auront un coefficient de defilement de 3 
this.background.AddBackground(1, @"ImagesTrees.png" , -500, 200, 3);

Si on compile et qu'on lance le projet, notre Background s'affiche bien... mais reste statique.
Nous allons donc simplement utiliser la méthode Update afin que l'utilisateur puisse interagir.

protected override void Update()
{
    int direction = 0;
    // On vérifie les touches pressées pour faire avancer l'écran 
    // (Fleche gauche ou droite) 
    Keys[] pressedKeys;
    if ((pressedKeys = Keyboard.GetState().GetPressedKeys()).Length != 0)
    {
        switch (pressedKeys[0])
        {
            case Keys.Left:
                direction = -1;
                break ;
            case Keys.Right:
                direction = 1;
                break ;
        }
    }
    // On affecte cette direction en tant que vitesse de 
    // défilement au background. 
    this .background.ScrollSpeed = direction;
    UpdateComponents();
} 

Si on recompile, le background réagit maintenant aux touches gauche et droite du pavé fléché.

Et voilà, notre ScrollingBackground est terminé !
Vous pouvez retrouver les sources complètes à cet endroit : http://codes-sources.commentcamarche.net/source/39790-xna-framework-creation-d-un-scrollingbackground

Liens Utiles

XNA Developer Center
Blog de l'équipe XNA
Télécharger XNA Game Studio Express Beta 1
Télécharger Visual C# Express (requis pour faire tourner XNA GSE 1)
Le SDK DirectX Utile pour les outils par encore intégrés dans XNA Game Studio (XACT, ...)

A voir également
Rejoignez-nous