Astuces DotNet (Sébastien Courtois)

01/09/2011

[XNA 4] Tutoriel 6 : Sprites animés (Partie 1 : Une animation / frame size fixe)

Filed under: .NET, C#, Débutant, XNA — Étiquettes : , , , , , — sebastiencourtois @ 21:27

Lors d’un tutoriel précédent, nous avons vu comment  afficher et déplacer des images. Nous allons maintenant voir comment afficher des animations pré-dessinées.

Tout commence par un fichier image (aussi appelé spritesheet) contenant l’ensemble des étapes de l’animation.

Jump

Exemple d’un personnage sautant (source : XNA App Hub )

explosion

Exemple d’animation d’explosion

Comme on peut le voir, l’animation est décompose en plusieurs étapes et il suffit de couper chacune des étapes (frames) puis de les afficher dans l’ordre a intervalle de temps régulier pour reproduire l’animation. Les frames sont de la même taille dans les exemples de ce post (ce ne sera pas toujours le cas comme nous le verrons dans des  prochains posts).

  • Présentation du projet de démonstration

Pour ce tutoriel, nous partirons d’un nouveau projet XNA 4 (Windows Game) auquel on va ajouter un autre projet utilitaire (Windows Game Library) nomme .Utils. Ce projet contiendra l’ensemble des classes nécessaires au chargement et a l’utilisation des images animes. Ce projet contiendra deux classes : SimpleAnimationDefinition et SimpleAnimationSprite.

Dans le même temps, on ajoute les différentes images (spritesheets) dans le projet Content de la solution. Vous pouvez trouver ces images ici : Tutoriel6a-SimpleAnimationContent.

Si tout se passe bien, votre solution devrait ressembler à ceci.

xnaT6a-projects

  • Définition d’une animation : Classe SimpleAnimationDefinition

Cet classe va permettre de paramétrer l’application en fournissant les informations nécessaires au gestion d’animation.

public class SimpleAnimationDefinition {
    public string AssetName { get; set; }
    public Point FrameSize  { get; set; }
    public Point NbFrames   { get; set; }
    public int FrameRate    { get; set; }
    public bool Loop        { get; set; }
}

La propriété AssetName indique le “mot clé” (AssetName défini dans le projet Content) de l’image contenant l’animation. Cela permettra de charger l’image en utilisant Content.Load<Texture2D>(…).

La propriété FrameSize définie la taille d’une frame de l’animation. Il faut souvent utiliser un logiciel de dessin type Paint pour connaitre la taille des frames (dans le cas du personnage qui saute, chaque frame fait 64 pixels sur 64 pixels. Pour l’explosion : 256 pixels sur 128 pixels). Nous verrons dans des posts suivants, comment gérer des animations dont les frames sont de tailles différentes.

La propriété FrameSize définie le nombre de frame de l’animation. Dans le cas du personnage, l’animation est compose de 11 frames. Pour cette propriété, on utilise un structure Point car certaines animations peuvent avoir des frames les uns en dessous des autres (comme c’est le cas pour l’explosion ou il y a 3 colonnes et 4 lignes de frames).

La propriété FrameRate définie la vitesse de l’animation. Par défaut, l’animation d’une application XNA est limite a 60 images par secondes mais nous verrons que, selon les fichiers, il est nécessaire de changer ce nombre d’images par secondes. Si on prend l’exemple du personnage contenant 11 frames, si l’on conserve un framerate de 60 images par secondes, l’animation complète prendra environ 16 ms.

La propriete Loop indique si l’on souhaite arrêter l’animation à la fin de celle-ci ou si l’on souhaite recommencer l’animation du début automatiquement.

new SimpleAnimationDefinition()
{
     AssetName = "explosion",
     FrameRate = 20,
     FrameSize = new Point(256,128),
     Loop = true,
     NbFrames = new Point(3,4)
}

Exemple de la configuration de l’animation explosion

  • Gestionnaire d’animation : Classe SimpleAnimationSprite

Cette classe va s’occuper de gérer une animation (charger l’image, initialiser les données, afficher les images …). Il sera initialisé grâce a un SimpleAnimationDefinition.

  • Proprietes de la classe :
public Point Position;
protected Game Game;
protected SimpleAnimationDefinition Definition;
protected SpriteBatch spriteBatch;
protected Texture2D sprite;
protected Point CurrentFrame;
protected bool FinishedAnimation = false;
protected double TimeBetweenFrame = 16; // 60 fps 
protected double lastFrameUpdatedTime = 0;

Position : Position de l’image dans l’écran

Game : Instance vers l’instance de la classe Game (utile pour charger l’image, créer un spriteBatch, avoir accès au GraphicsDevice…).

Definition : Définition de l’animation (voir paragraphe précédent)

spriteBatch : instance nécessaire pour afficher des images a l’écran (voir tutoriel 2).

sprite : Image

CurrentFrame : Position de la frame en cours d’affichage. Ce propriété sera utilisé pour calculer la partie de l’image/animation à afficher à l’écran.

FinishedAnimation : Indique si l’animation est terminé (cas d’une animation non boucle).

lastFrameUpdatedTime : Nombre de millisecondes depuis le dernier changement de frame

TimeBetweenFrame : Temps entre deux changements de frame (calculé grâce au Framerate).

Note : J’ai aussi rajouté deux propriétés pour gérer dynamiquement le framerate de l’animation. La première propriété ( private int _Framerate) contient juste la valeur du framerate. La deuxième est une propriété plus complexe défini comme suit :

private int _Framerate = 60;
public int Framerate
{
    get { return this._Framerate; }
    set {
       if (value <= 0)
           throw new ArgumentOutOfRangeException("Framerate can't be less or equal to 0");
       if (this._Framerate != value)
       {
          this._Framerate = value;
          this.TimeBetweenFrame = 1000.0d / (double)this._Framerate;
       }
   }
}

Cette propriété renvoie le framerate de l’animation mais permet aussi de définir ce framerate. Si on souhaite modifier ce framerate, la propriété va vérifier la valeur et modifier la propriété TimeBetweenFrame en consequence. J’utilise cette astuce afin d’éviter de faire la division 1000/Framerate a chaque Update pour connaitre d’éviter de temps entre deux frames.

  • Méthodes de la classe :

Voici le squelette de la classe.

public class SimpleAnimationSprite {
    /* Proprietes */ public SimpleAnimationSprite(Game game, SimpleAnimationDefinition definition)
    {
        /* Constructeur */ }

    public void Initialize()
    {
        /* Initialisation */ }

    public void LoadContent(SpriteBatch spritebatch = null)
    {
        /* Chargements des donnees */ }

    public void Reset()
    {
        /* Reinitialisation de l'animation */ }

    public void Update(GameTime time)
    {
        /* Mise a jour des donnees en vue de l'affichage */ }

    public void Draw(GameTime time, bool DoBeginEnd = true)
    {
        /* Affichage de l'animation */ }
}

Remarque : J’ai choisi de conserver une architecture proche de XNA pour construire la classe (Initialize,LoadContent,Update,Draw) afin de faciliter l’utilisation de celle-ci. Toutefois, certaines méthodes sont peut-être inutiles dans ce cas mais je pense que c’est une bonne manière de concevoir des classes réutilisables XNA. Je ne dérive pas de GameComponent car je veux avoir la liberté d’appeler mes classes comme je le souhaite (j’ai aussi lu des problèmes de performances possibles avec les GameComponent donc je privilégie la liberté d’utilisation et la performance).

Nous allons voir l’initialisation de la classe :

public SimpleAnimationSprite(Game game, SimpleAnimationDefinition definition)
{
    this.Game = game;
    this.Definition = definition;
    this.Position = new Point();
    this.CurrentFrame = new Point();
}

public void Initialize()
{
    this.Framerate = this.Definition.FrameRate;
}

public void Reset()
{
    this.CurrentFrame = new Point();
    this.FinishedAnimation = false;
    this.lastFrameUpdatedTime = 0;
}

L’idée est de définir le framerate et de déterminer la prochaine image à afficher comme étant la première du spritesheet (new Point() <=> X = 0,Y = 0) et en initialisant le framerate en fonction de la structure de définition.

public void LoadContent(SpriteBatch spritebatch = null)
{
    this.sprite = this.Game.Content.Load<Texture2D>(this.Definition.AssetName);
    if (spritebatch == null)
        this.spriteBatch = new SpriteBatch(this.Game.GraphicsDevice);
    else
        this.spriteBatch = spritebatch;
}

La méthode est assez triviale. On charge le spritesheet en tant que Texture2d et récupère ou crée  le SpriteBatch pour l’afficher dans la methode Draw. La raison pour laquelle j’autorise la création d’un SprtieBatch en dehors de la classe est par soucis d’optimisation et de réutilisation des ressources.

Les deux méthodes les plus intéressantes sont Update (qui va decider quel est l’image du spritesheet a afficher) et Draw pour l’afficher.

public void Update(GameTime time)
{
    if (FinishedAnimation) return;
    this.lastFrameUpdatedTime += time.ElapsedGameTime.Milliseconds;
    if (this.lastFrameUpdatedTime > this.TimeBetweenFrame)
    {
        this.lastFrameUpdatedTime = 0;
        if (this.Definition.Loop)
        {
            this.CurrentFrame.X++;
            if (this.CurrentFrame.X >= this.Definition.NbFrames.X)
            {
                this.CurrentFrame.X = 0;
                this.CurrentFrame.Y++;
                if (this.CurrentFrame.Y >= this.Definition.NbFrames.Y)
                    this.CurrentFrame.Y = 0;
            }
        }
        else
        {
            this.CurrentFrame.X++;
            if (this.CurrentFrame.X >= this.Definition.NbFrames.X)
            {
                this.CurrentFrame.X = 0;
                this.CurrentFrame.Y++;
                if (this.CurrentFrame.Y >= this.Definition.NbFrames.Y)
                {
                    this.CurrentFrame.X = this.Definition.NbFrames.X - 1;
                    this.CurrentFrame.Y = this.Definition.NbFrames.Y - 1;
                    this.FinishedAnimation = true;
                }
            }
        }
    }
}

Le choix de l’image à afficher se fait en utilisant le timer de XNA. On regarde si le temps depuis l’affichage de la dernière frame de l’animation est plus grand que le temps entre les frames (défini par le framerate). Si c’est le cas, on passe au frame de l’animation suivant. Le code parait complexe car on prend en compte les images “Carre” (i.e quand les frames sont stockes sur plusieurs lignes). Si l’on souhaite que l’animation tourne en boucle, on revient au premier frame une fois que l’animation est terminée.

public void Draw(GameTime time, bool DoBeginEnd = true)
{
    if(DoBeginEnd) this.spriteBatch.Begin();

    this.spriteBatch.Draw(this.sprite,
                          new Rectangle(this.Position.X, this.Position.Y, this.Definition.FrameSize.X, this.Definition.FrameSize.Y),
                          new Rectangle(this.CurrentFrame.X * this.Definition.FrameSize.X, this.CurrentFrame.Y * this.Definition.FrameSize.Y, this.Definition.FrameSize.X, this.Definition.FrameSize.Y),
                          Color.White);

    if (DoBeginEnd) this.spriteBatch.End();
}

L’affichage est relativement simple et se résumer a l’affichage d’une texture 2d par un spritebatch. La différence avec les tutoriaux précédents est que nous allons utiliser uniquement une partie de la texture (alors que d’habitude, on utilise l’ensemble de la texture). Pour cela, on utilise une méthode surchargée de Draw du SpriteBatch : SpriteBatch.Draw (Texture2D texture, Rectangle destinationRectangle, Nullable<Rectangle> sourceRectangle, Color color ). Ainsi, on joue sur le SourceRectangle pour choisir la partie de la texture que l’on souhaite afficher. Le rectangle de destination dépend de la position (point haut gauche) et de la taille de la frame.

Remarque : Le DoBeginEnd est un paramètre d’optimisation afin d’éviter d’utiliser la méthode Begin/End de multiple fois (==> perte de performances). Si DoBeginEnd = false, alors la SpriteBatch a déjà été Begin dans la méthode Draw de Game et ne doit pas être réouvert de nouveau. Cela permet de mutualiser les appels aux GPU (rappel : SpriteBatch est un Vertex/Pixel Shader).

  • Utilisation des animations dans le jeu

Nous allons maintenant utiliser la classe SimpleAnimationSprite dans la classe Game. On commence par ajouter la propriété qui va stocker les SimpleAnimationSprite. Notre exemple affichera 4 animationsmême temps donc celles-ci seront stockées dans un tableau.

public class Game1 : Microsoft.Xna.Framework.Game
{
   GraphicsDeviceManager graphics;
   SpriteBatch spriteBatch;
   SimpleAnimationSprite[] AnimManSprites;
   ....
}

La methode Initialize va creer et configurer les differents animations en utilisant les structures SimpleAnimationDefinition.

protected override void Initialize()
{
    this.AnimManSprites = new SimpleAnimationSprite[4];
    this.AnimManSprites[0] = new SimpleAnimationSprite(this, new SimpleAnimationDefinition()
    {
        AssetName = "Celebrate",
        FrameRate = 15,
        FrameSize = new Point(64, 64),
        Loop = true,
        NbFrames = new Point(11, 1)
    });
    this.AnimManSprites[0].Position.X = 250;
    this.AnimManSprites[0].Position.Y = 50;

    this.AnimManSprites[1] = new SimpleAnimationSprite(this, new SimpleAnimationDefinition()
    {
        AssetName = "Jump",
        FrameRate = 15,
        FrameSize = new Point(64, 64),
        Loop = true,
        NbFrames = new Point(11, 1)
    });
    this.AnimManSprites[1].Position.X = 300;
    this.AnimManSprites[1].Position.Y = 50;

    this.AnimManSprites[2] = new SimpleAnimationSprite(this, new SimpleAnimationDefinition()
    {
        AssetName = "Run",
        FrameRate = 15,
        FrameSize = new Point(64, 64),
        Loop = true,
        NbFrames = new Point(10, 1)
    });
    this.AnimManSprites[2].Position.X = 350;
    this.AnimManSprites[2].Position.Y = 50;

    this.AnimManSprites[3] = new SimpleAnimationSprite(this, new SimpleAnimationDefinition()
    {
        AssetName = "explosion",
        FrameRate = 20,
        FrameSize = new Point(256, 128),
        Loop = true,
        NbFrames = new Point(3, 4)
    });

    foreach (SimpleAnimationSprite anim in this.AnimManSprites)
        anim.Initialize();

    base.Initialize();
}

Remarque : Les valeurs des FrameSize et NbFrames dépendent des textures utilisées.

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    foreach (SimpleAnimationSprite anim in this.AnimManSprites)
        anim.LoadContent(spriteBatch);
}

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();
    foreach (SimpleAnimationSprite anim in this.AnimManSprites)
        anim.Update(gameTime);
    base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    this.spriteBatch.Begin();
    foreach (SimpleAnimationSprite anim in this.AnimManSprites)
        anim.Draw(gameTime,false);
    this.spriteBatch.End();
    base.Draw(gameTime);
}

L’architecture de la classe SimpleAnimationSprite permet d’avoir des méthodes LoadContent,Update,Draw relativement simple. L’ensemble du code est fait dans Initialize (appelée une seule fois) pour réduire le code des méthodes Update/Draw (appelées 60 fois par secondes au moins). En jeu vidéo, tout est affaire d’optimisation.

Tutorial6aFinal

Résultat a la fin de ce tutorial (anime normalement Smile with tongue out)

  • Conclusion

Vous pouvez maintenant afficher et animer des images en utilisant des spritesheets dont  les frames ont la même taille. Nous verrons plus tard des cas plus complexe avec des spritesheet comme celle de Prince Of Persia 2.

Projet complet (Tutorial 6a)

Créez un site Web ou un blog gratuitement sur WordPress.com.