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... ) 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..
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.
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.
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 ! Si ça, c'est pas 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 !).
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.
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. 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 ). 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 ).
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() ). 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 :
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.
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 ! ) 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 ! 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...
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...
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é.