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)

12 commentaires »

  1. Article très intéressant aussi bien pour la méthodologie que les compléments d’info🙂

    Commentaire par ophidia — 14/09/2011 @ 18:53

  2. Bonjour,
    Article très intéressant ! J’ai cependant un léger problème, étant en plein codage d’un space invaders like, j’aurais aimé faire apparaitre une explosion lorsque que le laser touche un ennemi.. Hors, comment faire ? Faut-il faire plusieurs Update et plusieurs Draw ( pour rechercher à quelle image on est, puis la dessiné et quand c’est finit la désafficher) . Merci d’avance pour ton aide, et encore une fois : super tuto !

    Commentaire par Yopp — 09/04/2012 @ 02:26

    • Une idee pourrait etre de creer une classe Explosion avec un Update et un Draw que tu pourra mettre dans ta liste des objets a afficher. Lors de la fin de l’animation (qui peut etre declenche par un event), tu l enleve de la liste ou tu le detruit. Il y a aussi la solution de la factory si tu as beaucoup d’explosion en meme temps.

      En esperant que cela puisse t’aider. Bonne chance avec ton projet.

      Commentaire par sebastiencourtois — 09/04/2012 @ 02:34

      • Dois faire autant d’Update qu’il n’y a d’image ? Lors de la collision entre un ennemi et mon laser, je fais un Update si le robot est mort et je fais un Draw ( même test, si le robot est mort ) .. Mais cela ne m’affiche qu’une seule image de l’explosion, et de plus elle ne disparait plus une fois afficher.. JE suis un peu perdu !

        Commentaire par Yopp — 09/04/2012 @ 14:07

  3. Aprés une rude bataille, j’ai presque réussi. Un seul problème persiste, j’ai déclarer le booléen FinishedAnimation en public dans la classe SimpleSpriteAnimation pour pouvoir y accéder. Je fais dans Draw un test : si le robot est touché , alors update et draw de l’explosion puis un deuxième test : Est ce que l’explosion est finie ( FinishedAnimation == true) , si oui je la remet a zéro. Mais en affichant avec un SpriteFont la valeur du booléen, il est toujours faux. Peux tu m’éclairer ? Merci encore !🙂

    Commentaire par Yopp — 09/04/2012 @ 16:40

    • Sans un morceau de code, je vais avoir du mal a te dire.
      Fais attention au fait que l’animation depend du temps donc fais gaffe au condition de fin de ton animation (utilise le >= plutot que le ==).

      Commentaire par sebastiencourtois — 10/04/2012 @ 02:44

  4. Un tres bon tutoriel bien expliqué, j’attends avec impatience la suite sur les sprites à dimensions variables !
    Comment cela se passe-t-il en gros ? A-t-on besoin d’une serie de coordonnees pour chaque frame, le tout stocké dans un Vector ? ou bien y a-t-il un moyen de faciliter le travail par la programmation ? (avec une detection au pixel par exemple)

    Commentaire par RadDreamer — 03/09/2012 @ 03:40

    • Bonjour,

      Merci pour ton avis sur le tutoriel🙂. Malheureusement il ne devrait pas y avoir de nouveaux tutoriaux XNA car :
      1) XNA est une technologie morte a court terme (pour le developpement PC/Console… en attendant de voir ce qu il y aura en next-gen).
      2) Je travaille pour Ubisoft Montreal (Assassin’s Creed 3 / Far Cry 3…) depuis quelques mois maintenant et je n’ai malheureusement beaucoup moins le temps de faire des articles.

      Pour la solution que j’avais prototype, c’etait un listing xml contenant les series de coordonnes. L’idee etait d’aprendre a utiliser le Content Pipeline XNA pour que ce fichier xml soit processe a la compilation. Suite a mon demenagement de San Francisco a Montreal, j’ai perdu une partie de mes donnees dont le draft de l’article donc, malheureusement, je ne pourrais pas te donner plus d’aide :s.

      Commentaire par sebastiencourtois — 03/09/2012 @ 03:46

      • Bonsoir,
        Je viens de voir une solution similaire a base de XML sur un autre blog.
        XNA en terme de développement console semble en effet limite (surtout ici au Japon ou les jeux xbox et le jeu pc sont des choses pratiquement inexistantes), mais quand on est dans l’amateur, ça parait une bonne plate-forme pour faire son premier jeu.
        En tout cas tes billets m’ont appris pas mal de choses (débarquant de l’AS3, on s’y perd un peu en C#)

        Commentaire par RadDreamer — 03/09/2012 @ 16:30

      • Oui XNA est bien pour apprendre les concepts mais pas plus. Apres il vaut mieux se concentrer sur du vrai DirectX soit en C++ soit en C# a travers des wrapper type SlimDX ou SharpDX surtout si tu souhaites rentrer dans l’industrie du jeu apres (ou XNA est inexistant et inconnu).

        Commentaire par sebastiencourtois — 03/09/2012 @ 16:45

  5. Hello, merci pour les tuto que je suivai, jusqu’à ce que tu dises que XNA est mort ?
    Je développe un jeu pour windows phone, et du coup j’aurai aimé savoir quel techno est intéressante pour les nouveaux IOS windows 8 ??

    Commentaire par tesan — 21/11/2012 @ 16:19

    • XNA n’est toujours pas officielment mort mais il n’y a pas eu de nouvelle version depuis un moment et j’etais a la Build l’an dernier lors de l’annonce de Win8 et clairement les dev MS disait de migrer vers DirectX pour le développement PC.
      Pour le Mobile, sur Win7, XNA reste la norme pour la 3d. Pour WP8, je te conseille plus d’utiliser SharpDX (http://www.c2i.fr/actualites/nouvelle-version-de-sharpdx-compatible-windows-phone-8) que XNA car tu aura plus de controle sur le materiel et de meilleure performance.

      Apres il faut voir ce que tu veux. XNA a beaucoup de classses/methodes d’aide pour le chargement de modeles, les shaders et tout ce qui permet de creer des applications 2d/3d rapidement.
      Maintenant Microsoft pousse a fond DirectX sur toute les plateforme (WP8, Win8 notamment avec WinRT…) et je pense que c’est la voie a suivre pour l’avenir. De plus, si tu souhaite integrer un jour un studio de jeu video, ta candidature aura plus de poids avec du DirectX que du XNA.

      Commentaire par sebastiencourtois — 21/11/2012 @ 16:32


RSS feed for comments on this post. TrackBack URI

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

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

%d blogueurs aiment cette page :