XNA

Débuter un jeu XNA

Préambule

Cette partie de l'exposé vous expliquera comment commencer le développement de votre jeu vidéo. Il est nécessaire d'avoir quelques connaissances en C# pour continuer ce tutoriel. Si vous êtes plutôt un développeur Java, sachez qu'il n'est pas difficile de s'adapter à ce langage. Une maitrise de l'IDE de Microsoft est souhaitable.

Comme dit précédemment, il vous faut l'IDE Visual Studio ou Visual C# ainsi que le framework XNA. Voici les différents liens permettant de les télécharger:


L'installation ne comporte pas de difficultés particulières et ressemble à une installation de logiciel classique sous Windows. Une fois l'installation terminée, il faut aller dans le menu " démarrer ", puis cliquer sur " Microsoft XNA Game Studio 3.1 " et enfin sur " Microsoft Visual C# 2008 Express Edition " (ou équivalent).

Ceci vous permettra de lancer l'IDE en ayant un environnement optimisé pour la création de votre jeu XNA. Créez votre premier projet en cliquant sur " Fichier " puis sur " Nouveau projet ". Vous devriez aboutir à cette fenêtre :

Il vous est donc possible de créer pour différentes plateformes, mais aussi des bibliothèques(libraries) pour les différentes plateformes Microsoft. Il est aussi possible de reprendre un projet fourni par XNA, via " Platformer Starter Kit ". Ce petit jeu vous offre un exemple concret des possibilités offertes par XNA et permet aussi de comprendre l'architecture d'un jeu XNA.

Notez que Windows Phone n'apparait pas sur les modèles de projet proposés. En effet, le développement de jeux XNA sur cette plateforme n'est disponible qu'à partir de la version 4.0 de XNA.

Dans cet exemple, nous allons créer un jeu pour PC. Vous devriez aboutir à l'écran suivant :

Sur la gauche, vous avez les éléments suivants :

Voici ci-dessous, le contenu de la classe Game1.cs :

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace WindowsGame1
{
    /// 
    /// This is the main type for your game
    /// 
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

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

        /// 
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// 
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// 
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// 
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
        }

        /// 
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// 
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// 

        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// 
        /// Provides a snapshot of timing values.
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        /// 
        /// This is called when the game should draw itself.
        /// 
        /// Provides a snapshot of timing values.
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }
    }
}
		
					

Celle-ci contient plusieurs méthodes, attributs et tâches :

Premiers pas avec XNA

Pour commencer en douceur, nous souhaitons afficher simplement l'image suivante qui se déplacera de gauche à droite :

Dans un premier temps, il faut ajouter cette image à notre projet. Voici les opérations à effectuer :
Clic droit sur " Content " puis cliquer sur " Ajouter " et enfin sur " Ajouter un élément existant ". Un explorateur apparait, permettant de choisir l'élément à ajouter au jeu. Sélectionnez donc le ballon.

Il faut ensuite créer la classe Ballon.cs. Etant donné que ce ballon est un élément affichable à l'écran, elle va hériter de la classe DrawableGameComponent.

Tout d'abord, nous déclarons les attributs suivants :

private Texture2D texture; //texture de l'image
private Vector2 position; //vecteur pour la position de l'image
private int speedX; //vitesse de déplacement de l'image
private SpriteBatch spriteBatch; //spriteBatch de l'image
					

Nous déclarons ensuite le constructeur de notre élément :

public Ballon(Game game, Vector2 positionInitial, int speedX) : base(game)
{
    this.position = positionInitial;
    this.speedX = speedX;
    this.Game.Components.Add(this);
}	
					
Ce constructeur affecte les différentes variables passés en paramètres. A la fin du constructeur, le composant s'ajoute à la liste des composants du jeu via l'appel this.Game.Components.Add(this); Ceci permet donc d'automatiser les différents traitements de ce composant(initialize, update etc.). Cette ligne est une convention pour le développement des jeux XNA. En effet, ce n'est pas la classe Game qui ajoute le composant mais le composant qui s'ajoute au jeu. Ceci permet une meilleure réutilisabilité car il devient plus facile de transmettre ce composant à un autre développeur.

Il faut ensuite mettre en place la méthode initialize :

public override void Initialize()
{
    base.Initialize();
    spriteBatch = new SpriteBatch(GraphicsDevice);
}
					
Cette classe se contente pour l'instant d'initialiser le SpriteBatch.


Ensuite, la méthode LoadContent :

protected override void LoadContent()
{
    texture = Game.Content.Load(@"ballon");
    base.LoadContent();
}
Nous chargeons la texture grâce à la ligne suivante :
texture = Game.Content.Load(@"ballon");
Il est important de noter qu'il ne faut pas mettre l'extension du fichier lorsqu'on décrit le chemin d'une ressource. En effet, lorsqu'on importe une ressource, celle-ci est compressée et converti en un autre format. Par conséquent, vous ne pouvez pas inclure deux ressources avec le même nom mais des extensions différentes. Il faut noter que nous avons ici directement écrit le chemin de la texture " en dur " pour simplifier cet exposé. L'idéal aurait été de passer par un service, comme nous l'avons vu précédemment.


Ensuite, la méthode Update :

public override void Update(GameTime gameTime)
{
    position.X += speedX;
    base.Update(gameTime);
}
					
Cette méthode se contente de mettre à jour la position de l'image. La notion de GameTime (le paramètre de la fonction) sera abordé plus tard.


Et enfin, la méthode Draw :

public override void Draw(GameTime gameTime)
{
    spriteBatch.Begin();
    spriteBatch.Draw(texture, position, Color.White);
    spriteBatch.End();
    base.Draw(gameTime);
}					
					
spriteBatch.Begin() permet de prévenir la carte graphique que l'on va lui envoyer des données. spriteBatch.End() permet d'informer la carte graphique que l'envoi est terminé. La méthode Draw permet donc d'afficher notre texture. Nous lui passons en paramètre :
  • La texture à afficher
  • La position où elle doit être affichée
  • La teinte que l'on veut appliquer à la texture. LA couleur blanche signifie une absence de teinte.

Il ne vous reste plus qu'à instancier le ballon dans la méthode Initialize de la classe Game :
Ballon ballon = new Ballon(this, new Vector2(10, 10), 5);

Voici un aperçu de ce qui s'affiche à l'écran :

Il existe cependant un petit problème. En effet, vous ne maitrisez pas la fréquence d'appel de la méthode update. Celle-ci dépend de la puissance de la plateforme. Par conséquent, le ballon risque de se déplacer plus vite sur une machine plus puissante et moins vite sur une machine moins puissante.

C'est ici qu'intervient le gameTime passé en paramètre. En effet, via ce paramètre, il est possible de connaitre le temps qu'il s'est écoulé depuis le dernier appel à update ou le temps qu'il s'est écoulé depuis le début du jeu.

Nous allons donc modifier la Ballon. Dans un premier temps, nous ajoutons les deux attributs suivants :

private double previousTimeMoved = 0;  //mémorisation du time lors du dernier mouvement
private double timeBetweenTwoMoves = 200; //temps qu'il doit s'écouler entre deux déplacements					
					
La première variable mémorise le temps qu'il s'est écoulé depuis le dernier déplacement et la deuxième le nombre de millisecondes qu'il doit s'écouler entre deux déplacements.

Nous modifions donc la méthode update :

public override void Update(GameTime gameTime)
{
    double currentTime = gameTime.TotalGameTime.TotalMilliseconds;
    if (this.previousTimeMoved == 0)
    {
        this.previousTimeMoved = currentTime;
    }
    if (this.previousTimeMoved + this.timeBetweenTwoMoves < currentTime)
    {
        position.X += speedX;
        this.previousTimeMoved = currentTime;
    }     
    base.Update(gameTime);
}					
					
Maintenant, nous récupérons le temps qu'il s'est écoulé depuis le début du jeu. Si c'est le premier appel à update, nous initialisons previousTimeMoved. Et enfin, nous faisons en sorte de mettre à jour la position toutes les 100 millisecondes(contenu de la variable timeBetweenTwoMoves).

Les musiques et les sons reprennent la même logique, hormis la phase Draw qui, évidemment disparait. Il existe cependant une autre manière de les gérer mais celle-ci ne sera abordé dans cet exposé.

Gestion des contrôles : utilisation des Services

L'étape suivante consiste à permettre au joueur de contrôler ce ballon en appuyant sur le bouton de droite ou celui de gauche. Nous allons pour cela utiliser les Services. Notez qu'il n'est pas forcément nécessaire d'utiliser des Services.

Pour comprendre la manière dont on va utiliser les Services, considérons le schéma suivant :

Au moment de l'étape " update ", la classe Ballon cherchera un service qui pourra lui dire quelle action a été commandé par le joueur. Pour cela, ce service doit respecter un certain contrat. Ainsi, un service correspondant au contrat fourni par le joueur sera extrait. Ainsi le fonctionnement du ballon est complètement indépendant du type de contrôle utilisé. Le développeur pourra, s'il le souhaite, changer de contrôles ou de plateformes sans qu'il n'y ait d'impact sur la classe Ballon.

Le contrat à respecter sera symbolisé par une interface que nous allons nommer ControlManager :

interface ControlManager
{
    Boolean isLeft();
    Boolean isRight();
}
					
Ce contrat consiste à dire au client si oui ou non les boutons de droite et de gauche ont été appuyés.

Ensuite, nous allons créer un Service qui respecte ce contrat. Nous nommerons ce service ControlManagerImpl.

class ControlManagerImpl : Microsoft.Xna.Framework.GameComponent, ControlManager
{
	public ControlManagerImpl(Game game) : base(game)
    {
        game.Services.AddService(typeof(ControlManager), this);
    }

    private KeyboardState getKeyboardState()
    {
        return Keyboard.GetState();
    }

    bool ControlManager.isLeft()
    {
        return getKeyboardState().IsKeyDown(Keys.Left);
    }

    bool ControlManager.isRight()
    {
        return getKeyboardState().IsKeyDown(Keys.Right);
    }
}
				
Dans un premier temps, lorsqu'on instancie un service, celui-ci s'ajoute à la liste des Services de la classe Game (comme nous avions fait précédemment pour ballon).
Ce Service se charge dans un premier temps de récupérer l'état du clavier via la commande suivante : Keyboard.GetState(). Ensuite, via la méthode IsKeyDown(Keys.Left) et IsKeyDown(Keys.Right), il vérifiera si le joueur a appuyé sur la touche de gauche ou celle de droite.

Il faut donc ensuite modifier la méthode update de la classe Ballon :

public override void Update(GameTime gameTime)
{
    ControlManager cm = (ControlManager)Game.Services.GetService(typeof(ControlManager));
    double currentTime = gameTime.TotalGameTime.TotalMilliseconds;
    if (this.previousTimeMoved == 0)
    {
        this.previousTimeMoved = currentTime;
    }
    if (this.previousTimeMoved + this.timeBetweenTwoMoves < currentTime)
    {
        if (cm.isRight())
        {
            position.X += speedX;
        }
        else if (cm.isLeft())
        {
            position.X -= speedX;
        }
        this.previousTimeMoved = currentTime;
    }
          
    base.Update(gameTime);
}				
					
Grâce à la ligne ControlManager cm = (ControlManager)Game.Services.GetService(typeof(ControlManager));
nous demandons à Game de nous fournir un Service qui respecte le contrat défini dans ControlManager. Ensuite, nous appelons les méthodes isLeft et isRight pour savoir si le joueur a appuyé sur les touches droite et gauche.

Bien entendu, XNA ne se limite pas à afficher des images qui se déplacent. Le prochain chapitre vous offre un aperçu des possibilités de XNA.