XNA : Créer un mini jeu type Asteroid – partie 1

Bienvenue dans ce premier article consacré au Framework de développement de jeu XNA. Afin de cibler un maximum de personnes ce tutoriel sera axé multiplateforme, c’est à dire que vous pouvez le suivre que vous vouliez développer pour Windows, Linux ou même Mac. Dans un premier temps nous allons voir quelques objets de base avec XNA puis nous passerons vite à la réalisation d’un mini jeu de type « Asteroid ». A la fin de cet article vous pourrez déplacer un vaisseau en 2 dimensions dans l’espace et dans les prochains nous le ferons tirer, détruire des autres vaisseaux, nous compterons le score du joueurs…

La démo que nous allons créer au cours de cet article

Les ressources graphique

Nous n’utiliserons que 2 fichiers graphique dans cet article, un qui sera la texture du vaisseau et un autre qui sera le fond (le background) et qui représentera l’espace. Un petit « clique droit/Enregistrer sous » sur les 2 images suivantes et nous pouvons passer à la suite 😉

Le fond utilisé dans le jeu
Le vaisseau utilisé dans le jeu

Maintenant il faut copier ces 2 images dans le dossier Content de votre projet de jeu. Si vous utilisez XNA et Visual Studio il suffit simplement de faire glisser ces images sur le projet Content. Si vous utilisez MonoGame il faudra créer un dossier Content à côté de votre exécutable (donc typiquement dans le $Project/bin/{Release|Debug}/Content) et copier les images dedans. Voilà vous êtes prêts pour passer à la suite.

Le ContentPipeline

Que vous utilisiez XNA ou MonoGame cette partie est très importante car elle concerne la gestion des ressources. Lorsque vous créez un projet XNA avec Visual Studio, une solution avec 2 projets est créée, un projet de jeu et un projet de type Content. Le projet de type Content représente les ressources que votre jeu va utiliser (images, sons, effets, etc…), celles-ci seront converties (sérialisée en réalité) à la compilation en fichier .xnb, ainsi une image « background.jpg » deviendra « background.xnb ». Avec MonoGame il n’y a pas de conversion à la compilation alors pour certaines ressources comme les images il n’y a pas de conversion ce qui ne pose pas de problèmes, mais pour des ressources comme les fonts (police de caractère) là il faut ruser et utiliser des fichiers déjà convertis en .xnb . Ce n’est peut être pas encore très clair pour le moment mais ça le deviendra vite je vous rassure 😉

Dans la classe du jeu vous avez accès constament à un objet static nommé Content qui permet de charger du contenu depuis le dossier Content. Pour charger une image stockée à la racine de ce dossier nous utiliserons la méthode suivante :

Texture2D monImage = Content.Load("monImage");

Vous noterez que nous ne spécifions pas l’extension du fichier car c’est le ContentManager qui s’occupe de déterminer le type et la méthode d’ouverture dans XNA. Pour MonoGame c’est exactement pareil sauf qu’il n’y a pas de conversion en .xnb. Au final que vous utilisiez MonoGame ou XNA la méthode de chargement sera exactement identique, seules certaines ressources changerons (les sons et musiques, les fonts, etc… mais nous y reviendrons).

Les bases du jeu

La classe de base d’un jeu avec XNA doit dériver de Game et propose 3 objets notables :

  1. graphics qui est une instance de GraphicsDeviceManager et qui permet de configurer ou obtenir tous ce qui se rapporte à l’affichage ;
  2. Content qui est une référence statique vers le ContentManager et qui nous permet de charger des ressources depuis le dossier Content ;
  3. spriteBatch de type SpriteBatch qui va nous permettre d’afficher à l’écran des textures, des effets, etc…

Pour notre première partie de jeu nous allons devoir charger 2 textures : le fond et le vaisseau, dans XNA une image sera représentée par un objet de type Texture2D. Il faudra ensuite créer une variable de type Vector2 qui définira les coordonnées à l’écran du vaisseau car nous voulons le déplacer avec le clavier il faut donc pouvoir mémoriser ces coordonnées.

public class SpaceGame : Game
{
  graphicsDeviceManager graphics;
  SpriteBatch spriteBatch;

  Texture2D ship;
  Vector2 shipPosition;
  Texture2D backgroudSpace;

  // Constructeur et autres méthodes
}

Dans le constructeur on determine la taille et le titre de la fenêtre. On peut passer en plein écran en passant la propriété IsFullScreen à la valeur true.

graphics.PreferredBackBufferWidth = 640;
graphics.PreferredBackBufferHeight = 480;
graphics.IsFullScreen = false;
Window.Title = "Tutoriel MonoGame 01 : Space Game";

Ensuite dans la méthode LoadContent() nous devons charger le fond d’écran ainsi que le vaisseau. On en profite d’ailleurs pour initialiser la variable shipPosition afin de positionner le vaisseau centré en bas de l’écran. L’objet graphics permet de récupérer via ses propriétés la largeur et la hauteur de l’écran ce qui est bien pratique dans notre cas. L’instance d’un objet Texture2D permet elle aussi de récupérer les informations sur la taille de l’image, ces propriétés sont automatiquement renseignées à la création de l’objet.

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

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

  // Texture du vaisseau
  ship = Content.Load("ship");

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

  base.LoadContent();
}

Il faut aussi penser à libérer les ressources quand on a fini de les utiliser et la méthode UnloadContent() est là pour ça. La méthodes Dispose() des objets Texture2D permettent de libérer une ressource, donc dans notre cas nous avons 2 ressources à libérer à la sortie de l’application.

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

  base.UnloadContent();
}

On passe ensuite au déplacement du joueur

protected override void Update(GameTime gameTime)
{
  // On quitte le jeu
  if (Keyboard.GetState().IsKeyDown(Keys.Escape))
    this.Exit();

  // Déplacement du vaisseau
  if (Keyboard.GetState().IsKeyDown(Keys.Up))
    shipPosition.Y -= moveSpeed;

  // Suite du code de déplacement
}

Le déplacement au clavier est très simple et s’utilise grâce à l’objet statique Keyboard qui permet de lire son état actuel. La méthode IsKeyDown(Keys key) retournera true si la touche key est enfoncée et la méthode IsKeyUp(Keys key) retournera elle aussi true si la touche key est relâchée. L’énumération Keys contient toutes les touches du clavier, vous pouvez donc cibler la touche que vous voulez.

La manette de Xbox 360

Vous pouvez aussi utiliser la manette de Xbox 360 en utilisant cette fois ci l’objet statique GamePad, son fonctionnement est similaire à celui de Keyboard à la différence qu’il faut préciser le numéro de la manette. En effet sur PC ou Xbox 360 vous pouvez avoir jusqu’à 4 manettes actives en même temps et il faut donc indiquer à XNA quelle manette on veut cibler.

if (GamePad.GetState(PlayerIndex.One).IsButtonDown(Buttons.Start))
  // Faire quelque chose

L’énumération PlayerIndex permet de choisir la manette ciblée, vous noterez que vous pouvez faire pareil avec la méthode GetState de l’objet Keyboard car on peut en théorie avec plusieurs claviers actifs.

Et enfin : Le rendu

Nous voilà à la fin du tutoriel et du fichier 😉 ici nous allons afficher à l’écran le vaisseau et dessiner le fond d’écran. L’affichage se passe en plusieurs étapes :

  1. On efface le contenu de l’écran
  2. On commence le dessin avec SpriteBatch
  3. On dessine
  4. On termine le dessin avec SpriteBatch

L’objet spriteBatch contient les méthodes dont nous avons besoin pour afficher du contenu à l’écran. On commence donc par ouvrir un bloc de dessin :

spriteBatch.Begin();
// On dessine des choses sur l'écran
spriteBatch.End();

La méthode Draw() de l’objet spriteBatch va nous permettre d’afficher du contenu à l’écran, voici sa signature :

Draw(Texture2D texture, Vector2 position, Color color)

L’affichage du fond d’écran se fera aux coordonnées x = 0 et y = 0 (à partir du haut à gauche de la fenêtre), pour spécifier ces coordonnées nous ferons un appel à Vector2.Zero qui renvoi un objet de type Vector2 avec les valeurs x et y à 0 (ce dont on à besoin). Vous noterez que la méthode Draw() est largement surchargée et peut prendre tout un tas de paramètres, pour rester simple nous n’utiliserons que la première version ici mais je vous rassure on utilisera rapidement les autres surcharges.

On procède de la même manière pour afficher le vaisseau sauf que là on utilisera l’objet shipPosition ainsi le vaisseau sera dessiné aux coordonnées mises à jour dans la méthode update().

Code source de cette première partie

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MonoGameTutorial
{
  public class SpaceGame : Game
  {
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    Texture2D ship;
    Vector2 shipPosition;
    int moveSpeed;

    Texture2D backgroundSpace;

    public SpaceGame ()
    {
      graphics = new GraphicsDeviceManager(this);
      Content.RootDirectory = "Content";

      graphics.PreferredBackBufferWidth = 640;
      graphics.PreferredBackBufferHeight = 480;
      graphics.IsFullScreen = false;

      Window.Title = "Tutoriel MonoGame 01 : Space Game";
    }

    protected override void Initialize()
    {
      base.Initialize();
    }

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

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

      // Texture du vaisseau
      ship = Content.Load("ship");

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

      base.LoadContent();
    }

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

      base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
      // On quitte le jeu
      if (Keyboard.GetState().IsKeyDown(Keys.Escape))
        this.Exit();

      // Gestion de l'accélération du vaisseau
      if (Keyboard.GetState().IsKeyDown(Keys.LeftShift))
        moveSpeed = 5;
      else
        moveSpeed = 2;

      // Déplacement du vaisseau
      if (Keyboard.GetState().IsKeyDown(Keys.Up))
        shipPosition.Y -= moveSpeed;

      if (Keyboard.GetState().IsKeyDown(Keys.Down))
        shipPosition.Y += moveSpeed;

      if (Keyboard.GetState().IsKeyDown(Keys.Left))
        shipPosition.X -= moveSpeed;

      if (Keyboard.GetState().IsKeyDown(Keys.Right))
        shipPosition.X += moveSpeed;

      base.Update(gameTime);
    }

    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()
      spriteBatch.Draw(ship, shipPosition, Color.White);

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

      base.Draw(gameTime);
    }
  }
}

Vous remarquerez que j’ai ajouté une variable moveSpeed qui permet de choisir la vitesse de déplacement du vaisseau et j’ai aussi ajouté la possibilité d’accélérer la vitesse du vaisseau quand la touche shift est enfoncée.

Voilà qui termine la première partie de ce tutoriel sur les bases avec XNA/MonoGame. Ce qui est intéressent avec ce code est qu’il va passer partout, si vous êtes sous Linux et que vous voulez faire fonctionner votre jeu il faudra simplement le recompiler avec XNA et Visual Studio et à l’inverse si vous êtes sous Windows et que vous voulez le rendre compatible Linux ou Mac, il faudra copier les ressources images dans le dossier Content et recompiler votre projet avec MonoGame.