XNA : Création d’une classe Sprite

XNA propose un tas de classes et de structures pour nous faciliter la vie dans le développement mais il y a quand même un gros manque : Une classe Sprite. Au fait qu’est ce qu’un Sprite ? C’est une image qui est affichée à l’écran et qui a des propriétés telles qu’une position, une texture, etc.. La classe que nous allons créer va donc reprendre du code que nous avons été obligé d’écrire plusieurs fois dans les 2 premiers tutoriels. Nous avons déclaré 2 structures Vector2 pour gérer la position du laser ET du vaisseau par exemple alors qu’il serait préférable d’avoir une classe qui contient déjà toutes ces informations (vitesse, position, disponible, etc…)

La classe Sprite

Structure

public class Sprite
{
  // Référence vers la classe du jeu
  private Game e_game;

  protected Vector2 _position;
  protected Texture2D _texture;
  protected Vector2 _speed;
  protected bool _active;

  // Position à l'écran du Sprite
  public Vector2 Position
  {
    get { return _position; }
    set { _position = value; }
  }

  // Texture du Sprite
  public Texture2D Texture
  {
    get { return _texture; }
    set { _texture = value; }
  }

  // Vitesse
  public Vector2 Speed
  {
    get { return _speed; }
    set { _speed = value; }
  }

  // Largeur de l'image
  public int Width
  {
    get { return _texture.Width; }
  }

  // Hauteur de l'image
  public int Height
  {
    get { return _texture.Height; }
  }

  // Définie si le sprite est actif
  public bool Active
  {
    get { return _active; }
    set { _active = value; }
  }

  // Objet Game
  public Game Game
  {
    get { return e_game; }
  }

  // ContentManager pour charger les ressources
  public ContentManager Content
  {
    get { return e_game.Content; }
  }

  #region Constructeurs
  public Sprite (Game game) { }

  public Sprite (Game game, Vector2 position)
    : this(game) { }
  #endregion

  // --- Schéma classique du pattern GameState
  public virtual void Initialize() { }

  public virtual void LoadContent(string textureName) { }

  public virtual void UnloadContent() { }

  public virtual void Update(GameTime gameTime) { }

  public virtual void Draw(SpriteBatch spriteBatch) { }
}

Ce schéma est bien classique est il ressemble comme 2 goûtes d’eau à la classe Game. En fait on peut le voir comme un calque que l’on va déposer sur la classe Game, à chaque instance de cette classe il y aura un élément graphique qui sera affiché.

Cette classe va principalement nous permettre :

  1. Positionner un objet
  2. Lui attribuer une vitesse sur les axes X et Y
  3. Activer son affichage ou le désactiver

Bien entendu plus tard celle classe évoluera pour devenir beaucoup plus complexe mais le but aujourd’hui est d’encapsuler tout ce que l’on a vue, dans une classe utilitaire que vous pourrez utiliser sur chacun de vos projets. Les variables sont protected car nous devrons, plus tard dériver cette classe pour créer des types bien plus complexe qu’un Sprite.

Nous allons commencer les explications par la fin pour une fois et débuter avec la méthode Draw().

La méthode Draw

public virtual void Draw(SpriteBatch spriteBatch)
{
  if (_active)
    spriteBatch.Draw (_texture, _position, Color.White);
}

Si la variable _active vaut true alors on affiche le Sprite, cela nous permet de gérer la visibilité des éléments graphiques qui compose le jeu.

La méthode Update()

public virtual void Update(GameTime gameTime) { }

Elle reste vide car nous n’avons de base aucun traitement à faire dans cette méthode. Lorsque nous créerons des types plus spécialisés on surchargera cette méthode.

Les méthodes UnloadContent et LoadContent

UnloadContent

public virtual void UnloadContent()
{
  if (_texture != null)
    _texture.Dispose();
}

On fait juste un test pour savoir si la texture est bien initialisée et si c’est le cas la méthode Dispose() est appelée pour libérer la ressource de la mémoire.

LoadContent

public virtual void LoadContent(string textureName)
{
  _texture = Content.Load(textureName);
}

La texture est chargée avec le nom passé en paramètre à cette méthode. A partir de là les propriétés this.Width et this.Height vont renvoyer les dimensions de la texture.

La méthode Initialize

public virtual void Initialize()
{
  _active = true;
}

Même si cela peut paraître redondant (et ça l’est) car on initialise déjà la variable _active à la valeur true dans le constructeur, cela est une bonne idée car la méthode Initialize() est faite pour réinitialiser un objet sans pour autant en créer une nouvelle instance.

Les constructeurs

public Sprite (Game game)
{
  e_game = game;
  _position = Vector2.Zero;
  _speed = Vector2.One;
  _active = true;
}

public Sprite (Game game, Vector2 position)
  : this(game)
{
  _position = position;
}

J’ai choisis de créer 2 constructeurs mais au final nous en aurons bien plus. Dans tous les cas un Sprite devra prendre comme paramètre une instance de Game (ou dérivée) afin que nous puissions avoir accès au ContentManager et à d’autres propriétés. Le premier constructeur est chargé d’initialisé les variables de la classe. Le deuxième constructeur appel le 1er (via l’utilisation de this()) et renseigne une position.

L’implémentation de cette nouvelle classe est terminée. Vous noterez que les méthodes sont toutes publiques car il faut que nous puissions les appeler en dehors de la classe, vous remarquerez aussi qu’elles sont virtuelles, nous pourrons ainsi les surcharger en dérivant cette classe. En effet nous aurons souvent besoin de l’héritage pour créer des Sprites spécialisés, par exemple créer un objet Hero, Enemy, Laser, Asteroid, etc…

Répercussions sur la classe de jeu

Nous allons maintenant utiliser cette classe dans notre projet « SpaceGame », je vous invite d’ailleurs à le retélécharger si vous ne l’avez pas sous la main. Je vous préviens il va y avoir du changement et pas qu’un petit peut 🙂

Ancien code
// Variables relatives au vaisseau
Texture2D ship;
Vector2 shipPosition;
int moveSpeed;
// Variables relatives au laser
Texture2D laser;
Vector2 laserPosition;
int laserSpeed;
bool shooted;
Nouveau code
// Vaisseau
Sprite ship;
// Laser
Sprite laser;

C’est pas mal du tout car on passe de 8 lignes à seulement 2 😀 La suite est tout aussi bien mais va demander beaucoup plus de changements.

protected override void Initialize()
{
  ship = new Sprite(this);
  ship.Speed = new Vector2(2, 2);
  laser = new Sprite(this);
  laser.Speed = new Vector2(4, 4);
  base.Initialize();
}

La méthode Initialize() va nous permettre de créer nos instances de ship et laser. On en profite pour initialiser les vitesses que l’on veut pour le vaisseau et le laser. Comme la vitesse est représentée par une structure de type Vector2 on passe une instance à cette structure à nos 2 objets. Vous noterez que l’on n’utilise pas encore la méthode Initialize de la classe Sprite car ici ce n’est pas nécessaire, cette méthode sera surtout utile (et je me répète désolé) quand nous créerons des types de Sprite plus complexes.

On charge et décharge les ressources

protected override void LoadContent()
{
  spriteBatch = new SpriteBatch(GraphicsDevice);

  // Fond de l'écran
  backgroundSpace = Content.Load("background-space");

  // Texture du vaisseau
  ship.LoadContent("ship");

  // Position initiale du vaisseau
  ship.Position = new Vector2(
	(graphics.PreferredBackBufferWidth / 2) - ship.Width / 2,
	graphics.PreferredBackBufferHeight - ship.Height * 2);

  laser.LoadContent("laser");

  base.LoadContent();
}

Sans grosses surprises on charge le contenu et on initialise la position du vaisseau. Si vous reprenez l’ancien code vous pourrez constater que l’ancienne variable de position pour le vaisseau était nommée shipPosition, là vous rajoutez un point entre ship et Position et vous obtenez le code que j’ai écris plus haut 😉 Et c’est le cas pour tout le reste du tutoriel.

// Libération des ressources
protected override void UnloadContent()
{
  backgroundSpace.Dispose();
  ship.UnloadContent();
  laser.UnloadContent();
  base.UnloadContent();
}

Toujours sans surprise les méthodes Dispose() sont remplacée par les méthodes UnloadContent(), il n’y a que pour le fond d’écran où c’est toujours pareil car lui n’est pas un Sprite (il pourrait).

Parce qu’il faut pouvoir tirer

private void shoot()
{
  if (!laser.Active)
  {
    laser.Position = new Vector2(
      (ship.Position.X + (ship.Width / 2)) - laser.Width / 2,
    ship.Position.Y);
    laser.Active = true; // Le laser est tiré !
  }
}

On utilise laser.Position à la place de laserPosition et ship.Position.X au lieu de shipPosition.X, le reste est identique. C’est marrent mais comme la classe Sprite à des propriétés Width et Height il n’y a pas de correction à faire sur ship.Width et ship.Height 😛 On utilise la valeur de la propriété Active de l’objet laser pour vérifié si on peut tirer ou pas.

L’affichage

protected override void Draw(GameTime gameTime)
{
  // Efface l'écran
  graphics.GraphicsDevice.Clear(Color.AliceBlue);

  // Début du mode dessin
  spriteBatch.Begin();

  // On affichage le fond à la position 0, 0
  spriteBatch.Draw(backgroundSpace, Vector2.Zero, Color.White);

  // On affiche le vaisseau à la position définie dans Update()
  ship.Draw(spriteBatch);

  // Si le laser est tiré on le dessine
  laser.Draw(spriteBatch);

  // Fin du mode dessin
  spriteBatch.End();

  base.Draw(gameTime);
}

Nous verrons très prochainement comment dériver cette classe pour créer une entité répondant encore plus à nos besoins. En tout cas le constat actuel est clair, nous avons économisé des lignes de code même si on a « un peu » complexifié le tout.

J’ai quelques soucis avec mon plugin d’affichage du code et la mise à jour de la méthode update me casse complètement l’affichage alors je suis obligé de vous demander de télécharger le projet de regarder ce qui a été fait. Il n’y a rien de compliqué on change shipPosition par ship.Position etc…

Télécharger le projet MonoGame (Windows)