Programmation graphique

Chapitre 5 : Les Sprites : Un programme élaboré d'animations

 

Tutoriel présenté par : Robert Gillard (Gondulzak)
Publication : 01 février 2014
Dernière mise à jour : 30 avril 2017

 

   Mise en œuvre des classes « Hero », « Animation » et « Game1 »    

   Soyons pratiques. Nous savons dès à présent que nous terminerons cette série de tutoriels par un jeu relativement élaboré de Chasse à la Sorcière (j'aime bien ces petits monstres... cheeky) et nous pourrons dès lors déjà réutiliser les classes que nous allons implémenter ici, même si nous devrons faire quelques modifications. Cependant, ne connaissant pas encore le nom du héros de cette « chasse du siècle », nous allons créer une classe générique « Hero » plutôt qu'une classe « Aron » même si nous utiliserons Aron sur sa moto (moto.png) pour cet exemple :

 

 

   Qu'allons-nous montrer dans cet exemple ? Et bien nous allons voir notre ami Aron se déplacer en moto avec les flèches du clavier ou avec le gamepad et nous ferons intervenir la gestion des collisions sur les côtés gauche et droit de l'écran. Rien d'exceptionnel pour l'instant, mais il y a quand même du travail et petit à petit nous construirons les classes nécessaires que nous réutiliserons plus tard pour l'élaboration de notre Jeu.. wink

Ceci étant dit, nous allons commencer par créer la classe « Hero ». A cet effet nous créons donc un nouveau projet que nous allons nommer par exemple « DriveIn ».

 

 

  LA CLASSE « Hero »


   Ouvrez votre nouveau projet, nous allons maintenant créer la classe « Hero ». Pour créer une nouvelle classe vous avez le choix entre deux solutions : 

1 - Vous pressez les touches SHIFT + ALT + C, cliquez sur « classe » dans la fenêtre centrale et entrez le nom « Hero.cs » dans la zone de saisie du bas puis cliquez ensuite sur Ajouter.  

2 – Dans la fenêtre de l'explorateur de solutions, à droite, faites un clic droit sur le projet « DriveIn » puis cliquez sur Ajouter classe, vous obtiendrez alors la même fenêtre que celle affichée ci-dessus.

 

 

   

   Après avoir cliqué sur Ajouter->classe, vous obtenez une nouvelle classe vide de tout code.
   Dans cette classe nous avons besoin des fonctions «Initialize», «Update» et «Draw».

   En haut du fichier, supprimez les instructions using et remplacez-les par celles-ci :

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

 

   Et à l'intérieur de la classe Hero, ajoutez les trois fonctions suivantes :

        
       class Hero
       {
          public void Initialize()
          {
          }

          
          public void Update()
          {         
          }

          public void Draw()
          {
          }
      }

   

    Nous allons maintenant entrer les variables qui nous servirons pour cette classe. A l'intérieur de la classe Hero, au-dessus de public void Initialize() , entrez cette suite de variables :

 

        
        // L'animation qui représentera le sprite
        public Animation HeroAnimation;

        // Position du sprite à partir du coin supérieur gauche de l'écran
        public Vector2 Position;

        // Un effet sur le sprite
        public SpriteEffects SpriteEffet;

        // Etat du sprite
        public bool Active;

        // Un accesseur permettant de retrouver la largeur d'une frame de l'animation
        public int Width
        {
            get { return HeroAnimation.FrameWidth; }
        }

        // Un accesseur permettant de retrouver la hauteur d'une frame de l'animation
        public int Height
        {
            get { return HeroAnimation.FrameHeight; }
        }

 

   Si vous remarquez que votre IDE souligne des erreurs en rouge, ne vous en faites pas pour le moment, celles-ci disparaitront quand nous aurons terminé l'implémentation des trois classes. wink

   Nous remarquons quelques nouveautés dans ces déclarations. Premièrement nous déclarons une variable HeroAnimation de type Animation, type qui est aussi une classe que nous allons implémenter par la suite.

Nous remarquons également deux fonctions comportant l'instruction get. Ce sont ce que l'on appelle des « accesseurs » en C# et qui justement permettent d'accéder à une variable depuis une classe différente.

 

   Nous devons maintenant compléter la fonction Initialize(). Supprimez-la et remplacez-la par celle-ci : 

 

        // Initialisation du sprite animé, de sa position et de l'effet désiré
        public void Initialize(Animation animation, Vector2 position, SpriteEffects 
                                                                         spriteEffet)               
        {
            HeroAnimation = animation;

            Position = position;
            SpriteEffet = spriteEffet;

            // On initialise le sprite comme étant actif
            Active = true;
        }

 

   A la fonction Initialize(), nous passons en paramètres les variables animation, position, et spriteEffet dont nous faisons des copies locales à l'intérieur de la fonction. Nous initialisons en outre la variable booléenne Active à true.

    Nous passons maintenant à la mise à jour de la fonction Update(). Supprimez cette fonction et remplacez-la par celle-ci :

 

        // Mise à jour de l'animation
        public void Update(GameTime gameTime)
        {
            HeroAnimation.Position = Position;
            HeroAnimation.spriteEffet = SpriteEffet;
            HeroAnimation.Update(gameTime);
        }

 

    Il s'agit ici de la mise à jour de la position et de l'effet sur les frames à chaque tour de boucle dans l'animation.

    Et pour en terminer avec cette classe, remplacez la fonction Draw() par celle-ci :

 

        // Dessine l'animation
        public void Draw(SpriteBatch spriteBatch)
        {
            HeroAnimation.Draw(spriteBatch);
        }

 

   Cette fonction sera celle qui dessinera notre animation. wink

 

   Voilà pour notre classe Hero. Nous allons maintenant approfondir pas mal de choses dans la classe Animation car c'est celle-ci qui va se charger de l'animation de notre sprite « moto.png » ou plutôt des frames que comporte celui-ci. Et vous pouvez déjà sauvegarder votre projet à cet instant.

   Ceci étant dit, nous pouvons passer à l'implémentation de la classe Animation. De la même manière que vous avez créé la classe Hero, pressez les touches SHIFT + ALT + C, cliquez sur « classe » dans la fenêtre centrale et entrez le nom « Animation.cs » dans la zone de saisie du bas puis cliquez ensuite sur Ajouter.

 

       La classe « Animation »

    C'est dans cette classe que va se faire l'animation de notre motocycliste. Dans notre précédent projet «AronWalk», l'animation d'Aron se faisait dans l'unique classe du projet, la classe Game1. Mais pour permettre une plus grande clarté d'un programme, cette classe ne peut pas être étendue à l'infini et c'est pourquoi nous avons crée une classe sprite (Hero) et que nous allons maintenant implémenter la classe « Animation ».

    Comme dans la classe Hero vous obtenez une nouvelle classe vide de tout code. Et dans cette classe nous avons également besoin des fonctions « Initialize », « Update » et « Draw ».

    Commençons par faire comme dans notre classe Hero. En haut du fichier, supprimez les instructions using et remplacez-les par celles-ci :   

   

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

 

   A l'intérieur de la classe Animation, ajoutez les variables suivantes :

 

        
       // La texture de notre de sprite
        Texture2D heroTexture;

        // L'échelle d'une frame
        float scale;

        // Temps écoulé depuis la dernière frame
        int timeSinceLastFrame;

        // Temps pendant lequel une frame est affichée
        int millisecondsPerFrame;

        // Nombre de frames de la feuille de sprites
        int numberOfFrames;

        // Le numéro de la frame courante
        int currentFrame;

        // La couleur de la frame qui sera affichée
        Color color;

        // Une surface de l'image source
        Rectangle sourceRect = new Rectangle();

        // Une surface pour afficher l'image
        Rectangle destinationRect = new Rectangle();

        // Largeur d'une frame
        public int FrameWidth;

        // Hauteur d'une frame
        public int FrameHeight;

        // Etat de l'animation (active/inactive)
        public bool Active;

        // Booléen qui détermine si une animation est en cours
        public bool Looping;

        // Position d'une frame
        public Vector2 Position;

        // Rotation d'une frame
        
        float rotation;

        // Origine d'une frame
        Vector2 origin;

        // Effet appliqué à une frame
        public SpriteEffects spriteEffet;

        // Profondeur 
        float depth;

 

   Nous allons initialiser toutes ces variables. Vous pouvez maintenant supprimer la fonction Initialize() et la remplacer par celle-ci :

 

        
        // Initialisation
        public void Initialize(Texture2D texture, Vector2 position, int frameWidth,
                               int frameHeight, int numberOfFrames,int millisecondsPerFrame, 
                                                     Color color, float scale, bool looping)
        {
            // Copies locales des valeurs passées à la fonction
            this.color = color;
            this.FrameWidth = frameWidth;
            this.FrameHeight = frameHeight;
            this.numberOfFrames = numberOfFrames;
            this.millisecondsPerFrame = millisecondsPerFrame;
      
            this.scale = scale;
            this.Position = position;
            heroTexture = texture;
            Looping = looping;
            
            // Initialisations
            rotation = 0f;
            origin = new Vector2(0, 0);
            depth = 1f;

            timeSinceLastFrame = 0;
            currentFrame = 0;

            // L'animation sera active par défaut
            Active = true;
        }

 

Note de Jay : Une fonction Initialize() n'est pas forcément nécessaire si vous ne souhaitez l'appeler qu'une seule fois au début du programme. Dans ce cas, créez simplement une première fonction du nom de votre classe, faites vos initialisations dedans, et le C# / XNA s'occuperont de tout initialiser au début du programme ! wink Si ça, c'est pas cool ?! cool

 

   Et de la même manière que pour la fonction Initialize() de la classe Hero, ici aussi nous passons une suite de variables en paramètres, variables dont nous créons une copie locale à l'intérieur de la fonction. Nous initialisons en outre les variables desquelles nous devons connaitre les valeurs initiales et nous activons l'animation à true (vrai) par défaut.

   Nous pouvons passer maintenant à la très importante fonction d'animation de notre sprite (ce sont en fait les différentes frames de notre feuille de sprites qui régissent notre animation smiley !).

 

   Il y a quelques explications que je voudrais apporter ici, avant l'affichage de la fonction Update(). Je pensais attendre l'implémentation de la fonction Game1 avant d'en parler mais je crois qu'il est plus opportun de le signaler ici.
  Notre feuille de sprite (moto.png) possède des dimensions de 320 x 55 pixels et comporte 4 frames de 80 x 55 pixels chacune. La première frame a bien la valeur 0 et non 1 : nous avons donc 4 frames dont la position courante (currentFrame) varie de 0 à 3 et leurs dimensions sont données par les variables FrameWidth et FrameHeight et l'échelle de la frame à afficher est représentée par la variable scale. Mais me direz-vous, où sont donc initialisées ces variables, nous n'en avons pas encore parlé...? C'est exact mais nous allons devoir attendre l'implémentation de notre classe Game1 pour le voir car c'est dans la fonction LoadContent() de la classe Game1 que nous allons initialiser ces variables mais ne vous en faites pas, tout cela sera plus clair lors de l'implémentation de notre dernière classe. wink

    Bien, ces précisions apportées, vous pouvez supprimer la fonction Update() et la remplacer par celle-ci

 

        
          // Mise à jour de l'animation
        public void Update(GameTime gameTime)
        {
            // Pas d'animation dans ce cas 
            if (Active == false)
                return;

            // Mise à jour du temps écoulé
            timeSinceLastFrame += (int)gameTime.ElapsedGameTime.TotalMilliseconds;


            //  Si le temps écoulé est supérieur au temps alloué à une frame
            if (timeSinceLastFrame > millisecondsPerFrame)
            {
                // On passe à la suivante
                currentFrame++;


                // Si la frame courante est égale au nombre de frames, on réinitialise 
                // celle-ci à 0 
                if (currentFrame == numberOfFrames)
                {
                    currentFrame = 0;
                    // Si on n'est pas en cours de boucle on désactive
                    if (Looping == false)
                        Active = false;
                }


                // On réinitialise le temps écoulé depuis la dernière frame
                timeSinceLastFrame = 0;
            }


            // On utilise la bonne frame en multipliant le numéro de la frame courante
            // par la largeur de la frame
            sourceRect = new Rectangle(currentFrame * FrameWidth, 0, FrameWidth,                                                                
                                                                          FrameHeight);
            
            // Que l'on affiche aux emplacements et à l'échelle voulus 
            destinationRect = new Rectangle((int)Position.X - (int)(FrameWidth * scale) / 2,
                                           (int)Position.Y - (int)(FrameHeight * scale) / 2,
                                                                  (int)(FrameWidth * scale),
                                                                (int)(FrameHeight * scale));
        }

 

   Il n'y a pas beaucoup de différences entre la fonction Update() du projet « AronWalk » et celle-ci. wink Voyons cela de plus près.


   Si la valeur de Active est false, il n'y a pas d'animation, donc pas d'affichage du sprite. On compare ensuite le temps écoulé depuis la dernière frame et le temps d'affichage alloué à une frame. Si le premier est supérieur au second on passe à la frame suivante jusqu'à ce que l'on atteigne le nombre de frames de la feuille de sprites. A ce moment, la frame courante est la dernière de la liste et on remet sa variable à 0 (je vous rappelle que la première frame a l'indice 0 cheeky). Finalement, on remet également le temps écoulé depuis la dernière frame à 0.

   Et nous utilisons la classe Rectangle pour faire défiler les frames. La position de la frame d'origine est toujours calculée en multipliant son index par la largeur de la frame exprimée en pixels et affichée dans le rectangle de destination représenté par sa position, sa largeur, sa hauteur ainsi que l'échelle appliquée à cette frame.

   Il ne nous reste plus qu'à dessiner l'animation. Supprimez la fonction Draw() et remplacez-la par celle-ci :

 

        
        // Dessine l'animation
        public void Draw(SpriteBatch spriteBatch)
        {
            // Uniquement si Active est "vrai"
            if (Active)
            {
                spriteBatch.Draw(heroTexture, destinationRect, sourceRect, color,
                                                                              
                                             rotation, origin, spriteEffet, depth);
            }
        }

 

   Nous utilisons ici une surcharge de la fonction Draw() comprenant huit paramètres parce que nous utilisons les rectangles destination et source pour l'affichage.

Je vous renvoie à la page web Xna MSDN en ce qui concerne les différentes méthodes Draw() surchargées de la classe SpriteBatch (on peut y accéder en faisant un clic droit sur la fonction depuis VS wink).

   Ceci étant dit, il nous reste à compléter la classe Game1 qui a été créée en même temps que notre projet DriveIn. Dans l'explorateur de solutions, à droite, faites un double-clic gauche sur le fichier Game1.cs afin de l'amener à l'avant-plan de votre IDE.


   Game1 est la classe principale de notre projet. C'est dans cette classe que nous allons charger les différents assets (ressources) nécessaires au dessin et à l'animation, et que nous implémenterons le déplacement de notre héros animé à l'aide des flèches du clavier ou des boutons du gamepad.

   Nous mettrons également un background en fond d'écran, ce qui nous changera quelque peu de nos fenêtres coloriées que nous avons présentées jusqu'ici. Voici le background en question, arrangé par Jay pour l'occasion :

 

 

                La classe « Game1 »

    A l'intérieur de public class Game1 : Microsoft.Xna.Framework.Game, en dessous de SpriteBatch spriteBatch;, nous introduirons les données suivantes :

 

        
        // Une texture pour notre fond d'écran
        Texture2D background;

        // Déclaration de hero 
        Hero hero;

        // Les états du clavier qui détermineront les touches pressées
        KeyboardState currentKeyboardState;
        KeyboardState previousKeyboardState;

        // Les états du gamepad qui détermineront les boutons pressés
        GamePadState currentGamePadState;
        GamePadState previousGamePadState;

        // Déclaration de la vitesse du héro
        float heroMoveSpeed;

 

   Il n'y a rien de spécial dans ces déclarations, à part que nous devons déclarer une variable hero représentant notre classe Hero que nous avons implémentée en premier. Et il n'y a rien à changer à notre constructeur qui met en oeuvre le système graphique.

   Maintenant, supprimez la fonction Initialize() que vous remplacerez par celle-ci :

 

        
        // Permet au jeu de s’initialiser avant le démarrage.
        // Emplacement pour la demande de services nécessaires et le chargement de contenu
        // non graphique. L'appel à base.Initialize passe en revue les composants
        // et les initialise.
        protected override void Initialize()
        {
            // TODO: Ajoutez la logique d'initialisation ici           
            graphics.PreferredBackBufferHeight = 480;
            graphics.PreferredBackBufferWidth = 800;

            // Initialise la classe Hero
            hero = new Hero();

            // Initialise une vitesse constante au héro
            heroMoveSpeed = 8.0f;
       
            base.Initialize();
        }

 

   C'est la fonction dans laquelle on initialise le buffer d'écran, et où l'on procède à une instanciation de la classe Hero. On donne une vitesse constante au déplacement de notre sprite : (heroMoveSpeed = 8.0f;).

Je parle bien ici de vitesse constante du déplacement quelle que soit la frame affichée et non pas d'une variable de type const appelée heroMoveSpeed car celle-ci aurait dû être initialisée dans la déclaration des variables. J'ajoute cette remarque ici car nous parlerons de vitesse et d'accélération de l'animation dans la dernière partie de ce tutoriel.

Ceci étant dit, on continue. Supprimez la fonction LoadContent() et remplacez-la par celle-ci. Nous donnerons ensuite les explications nécessaires : 

 

// LoadContent est appelé une fois par partie. Emplacement de chargement
// de tout votre contenu (assets, médias...).
protected override void LoadContent()
{
    // Crée un nouveau SpriteBatch qui sera utilisé pour dessiner les textures
    spriteBatch = new SpriteBatch(GraphicsDevice);
 
    // TODO: Utiliser this.Content pour charger le contenu de jeu ici
    // Load the player resources
    background = Content.Load<Texture2D>("Background22bis");
    Texture2D heroTexture = Content.Load<Texture2D>("moto");
 
    Animation heroAnimation = new Animation();
    heroAnimation.Initialize(heroTexture, Vector2.Zero, 80, 55, 4, 20,
    Color.White, 1.0f, true);
 
    Vector2 heroPosition = new Vector2(150, 417);
    hero.Initialize(heroAnimation, heroPosition, hero.SpriteEffet);
}

 

   Ici nous chargeons les ressources et notamment notre background et notre sprite (à ne pas oublier d'ajouter au projet Content() cheeky). Et, afin de permettre l'animation de notre sprite nous faisons une instanciation de la classe Animation avec notre variable heroAnimation que nous initialiserons immédiatement ensuite.

   Et c'est ici que nous initialisons les valeurs des variables de la fonction heroAnimation.Initialize(), et ce sont ces valeurs qui seront utilisées par la fonction Initialize() de notre classe Animation, c'est-à dire les valeurs passées aux variables représentant :

 
texturemoto.png
positionVector2.Zero
frameWidth : 80
frameHeight : 50
numberOfFrames : 4
millisecondsPerFrame : 20
color : Color.White
scale : 1.0f
looping : true

 

   Nous donnerons ensuite des valeurs à la position initiale du héros et nous initialiserons son animation, sa position ainsi que l'effet appliqué au sprite.

   Il nous faut maintenant faire une mise à jour de notre héro. Eh oui, il faut pouvoir déplacer son animation et c'est ici que nous allons le faire. Nous allons donc créer une nouvelle fonction que nous nommerons UpdateHero et que nous plaçerons soit avant soit après la fonction Update() de la classe Game1.

    A l'endroit que vous aurez choisi, entrez la fonction suivante : 

 

        
        private void UpdateHero(GameTime gameTime)
        {
            hero.Update(gameTime);

            // Utilisation du clavier / Dpad
            if (currentKeyboardState.IsKeyDown(Keys.Left) ||
            currentGamePadState.DPad.Left == ButtonState.Pressed)
            {
                hero.SpriteEffet = SpriteEffects.None;
                hero.Position.X -= heroMoveSpeed;
            }

            if (currentKeyboardState.IsKeyDown(Keys.Right) ||
            currentGamePadState.DPad.Right == ButtonState.Pressed)
            {
                hero.SpriteEffet = SpriteEffects.FlipHorizontally;
                hero.Position.X += heroMoveSpeed;
            }
 
            // On fait en sorte que le héro ne dépasse pas les bords de l'écran
            hero.Position.X = MathHelper.Clamp(hero.Position.X, 0 + hero.Width/2,
                                        GraphicsDevice.Viewport.Width - hero.Width/2);

        }

 

   Après avoir initialisé une instance de la classe hero, nous la mettons à jour ici. Ensuite, selon que l'on utilise le clavier ou le gamepad et ce, selon la direction désirée, nous dirigeons notre animation vers la gauche ou vers la droite en incrémentant sa position sur l'axe horizontal, d'une valeur égale à la vitesse que nous lui avons donnée dans la variable heroMoveSpeed. Nous testons ensuite la gestion des collisions sur les côtés gauche et droit de notre fenêtre grâce à une fonction de la classe MathHelper qui nous est fournie par Xna. smiley

   Il ne nous reste plus qu'à mettre à jour la fonction Update() de notre classe Game1 en y insérant la fonction que nous venons d'écrire.

   Supprimez la fonction protected override void Update(GameTime gameTime) et remplacez-la par celle-ci:

 

        
        // Permet au jeu d’exécuter la logique de mise à jour du monde,
        // de vérifier les collisions, de gérer les entrées et de lire l’audio.
        // < param name="gameTime">Fournit un aperçu des valeurs de temps.
        protected override void Update(GameTime gameTime)
        {
            // Permet la sortie du jeu
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Ajoutez la logique de mise à jour ici
            /* Sauve le précédent état du clavier et du gamepad afin que l'on puisse   
               déterminer quelle touche/bouton sera pressé */
            previousGamePadState = currentGamePadState;
            previousKeyboardState = currentKeyboardState;

            // Lit l'état courant du clavier ou du gamepad et l'enregistre
            currentKeyboardState = Keyboard.GetState();
            currentGamePadState = GamePad.GetState(PlayerIndex.One);

            // Mise à jour du héro
            UpdateHero(gameTime);

            base.Update(gameTime);
        }

 

  Cette fonction permet de sauvegarder les états précédents du clavier/gamepad afin que ceux-ci puissent être utilisés dans la fonction UpdateHero() mise à jour ici, juste avant l'instruction base.Update(gameTime); .

   Et nous terminons (enfin ! surprise) le tout par la mise à jour de notre fonction draw(). Supprimez cette fonction du fichier Game1 et remplacez-la par celle-ci

 

        
        // Fonction appelée quand le jeu doit se dessiner.
        // < param name="gameTime">Fournit un aperçu des valeurs de temps.
        protected override void Draw(GameTime gameTime)
        {
            //GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Ajouter le code du dessin ici
            spriteBatch.Begin();

            //Dessine le background
            spriteBatch.Draw(background, Vector2.Zero, Color.White);

            //Dessine le héro
            hero.Draw(spriteBatch);

            // Arrêt du dessin
            spriteBatch.End();

            base.Draw(gameTime);
        }

 

   Voilà, c'est tout et c'est déjà pas mal ! cheeky Peut-être es-ce un peu lourd à digérer en une fois mais voulant implémenter ces trois classes dans un seul projet afin de rendre visible le résultat en un chapitre, il m'aurait été impossible de faire plus court. Relisez le code afin d'assimiler les relations qui lient les classes entre elles, vous pourrez ensuite le réutiliser pour réaliser vos propres animations. Nous en reparlerons dès l'implémentation de nouvelles classes qui seront nécessaires à l'élaboration de notre jeu de chasse au monstre, ah ah... laugh

   Dans l'immédiat, sauvez votre projet et compilez-le en cliquant sur Déboguer - > Générer la solution. Pressez ensuite la touche F5 et admirez notre motocycliste se déplacer vers la gauche et vers la droite dans la campagne de Meruvia... wink

   Comme je l'ai annoncé dans le forum Xna, il était prévu que ce chapitre comprenne également la gestion d'une caméra, ainsi que les effets de zoom et de profondeur mais la longueur de celui-ci fait que ces éléments seront reportés dans de futurs chapitres.

   Dans le prochain chapitre, je vous présenterai un concept d'accélération dans la gestion d'une animation mais en attendant, je vous laisse admirer les captures d'écran prises lors du déplacement ne notre sprite animé.

 

 

 


 

 

 

 

 

Connexion

CoalaWeb Traffic

Today172
Yesterday282
This week966
This month3259
Total1742466

19/04/24