Programmation graphique

Chapitre 3 : Gestion des Inputs et déplacements

 

Tutoriel présenté par : Robert Gillard (Gondulzak)
Date d'écriture : 15 décembre 2014
Dernière mise à jour : 16 novembre 2015

 

   Préliminaires

    Ce troisième chapitre va nous permettre de gérer les différentes formes de déplacement de nos sprites, que ce soit avec le clavier du PC, ou le gamepad. cool

    Il va nous permettre en outre de nous initier aux couches (ou layers) et aux effets de profondeur en faisant passer une image devant ou derrière une autre (nous approfondirons ces différents effets dans les tutoriels suivants).

    Nous terminerons ce tutoriel par l'élaboration d'un mini jeu de Chasse à la Sorcière (Je vous l'avais promis, non...?). Bien sûr, à ce stade de nos tutoriels il ne s'agira vraiment que d'un mini jeu dont le code sera uniquement écrit dans la classe Game1(). Plus tard, au terme de cette série de tutoriels, nous développerons ce jeu en profondeur en ajoutant diverses classes, des sons, et autres explosions et en introduisant un héros combattant des sorcières dont les balais serviront de... fusils ou autres canons, alors accrochez-vous et commençons notre second tutoriel... devil

 

Téléchargez l'archive contenant les projets et les images nécessaires pour ce chapitre


    Avant d'entrer dans la « Gestion des Inputs » proprement dite, il y a deux choses dont je voudrais vous parler concernant l'affichage (et surtout d'un grand nombre d'affichages) de sprites à l'écran. Il s'agit du Double Buffering et du Frame Rate dont je vous donne ici quelques explications (mais ne craignez rien, ils ne vous veulent aucun mal...). sad

 

   I – Un peu de théorie...

           a – Le Double Buffering

 

     Imaginons que nous ayons à afficher un background sur notre fenêtre et qu'aussitôt nous devions également afficher un grand nombre de sprites ou d'images quelconques, recouvrant tout ou partie de notre fenêtre (ce qui est évidemment le but d'un jeu vidéo...). cheeky

     Le dessin des images à l'écran est très rapide mais l'oeil humain également et, avec un seul buffer (mémoire écran), nous verrions une succession d'images s'afficher les unes après les autres. C'est pourquoi les informaticiens ont pensé à créer un second buffer d'écran pour résoudre ce problème.

    Le buffer arrière ou back buffer est utilisé pour dessiner l'écran en mémoire graphique et quand celui-ci est prêt à être affiché, on « switche » le buffer arrière avec le buffer avant (front buffer), ainsi l'oeil de l'utilisateur ne peut voir tout l'écran dessiné, qu'une fois que toutes les images sont déjà en mémoire.

    Lors de notre premier tutoriel nous avons vu, dans notre constructeur, comment s'initialisait le système graphique :

 

            public Game1()
            {
              graphics = new GraphicsDeviceManager(this); // Initialise de système graphique
              
              Content.RootDirectory = "Content";
            }
        

 

     Il se fait que par défaut, Xna initalise le système graphique en affichant une fenêtre de dimensions standardisées (800 x 480), mais nous allons bientôt voir comment modifier les dimensions de nos fenêtres.


            b – Le Frame Rate

     Une seconde chose importante à ne pas négliger lors de la création d'un jeu vidéo est le Frame Rate ou nombre d'images (écrans) affichées par seconde. Il est en effet très utile de savoir, lors de la conception d'un jeu si celui-ci n'est pas à la limite de « ramer » et que le frame rate reste dans des valeurs convenables.

     Xna a déterminé un nombre de 60 fps/sec, ce qui veut dire qu'une frame devrait être dessinée environ toutes les 17 millisecondes (1000/60). Cependant ce nombre de frames pourrait chuter très vite (textures très détaillées, musiques, sons et autres calculs d'intelligence artificielle) et il est important de garder un oeil sur les fps lors de l'élaboration de jeux volumineux tout en prenant garde de ne pas descendre au dessous de 30 frames/seconde. crying

     Nous allons présenter un exemple simple de calcul de fps dans un programme qui ne comporte qu'un seul background (notez que le programme présenté ci-après est tout-à-fait indépendant de ce que vous auriez à afficher à l'écran).

 

           c – Un programme qui calcule le nombre de frames par seconde (FPS)

     Bien, à ce stade de notre tutoriel vous pouvez créer un nouveau projet Xna que vous nommerez « CalculeFPS » (vous savez comment créer un projet Xna maintenant...).

     Nous allons commencer par initialiser quelques variables. Dans la classe Game1, juste en dessous de SpriteBatch spriteBatch; , entrez les variables suivantes :

 

 double millisecondesEcoulees = 0;
 int nbFrames = 0;
 int fps = 0;

 Texture2D Background;

 

     Maintenant nous allons modifier notre fonction Initialize().

     Supprimez la et remplacez-la par celle-ci :

 

                protected override void Initialize()
                {
                   // TODO: Ajoutez la logique d'initialisation ici
                   graphics.PreferredBackBufferWidth = 800;
                   graphics.PreferredBackBufferHeight = 480;

                   graphics.ApplyChanges();
            
                   base.Initialize();
                }

 

     Notez que j'ai utilisé les dimensions 800 x 480 pour l'exemple mais vous pouvez choisir celles que vous voulez, en fonction de votre écran. Dans cette fonction nous choisissons donc un mode d'écran et nous indiquons à Xna d'appliquer les changements par l'instruction :

        graphics.ApplyChanges();

    Notez que pour un programme quelconque, il est toujours possible d'afficher le mode « Plein écran » en ajoutant l'instruction suivante dans la fonction Initialize() à la place des différentes options présentes : graphics.IsFullScreen = true; , mais que ceci n'aurait aucun sens dans notre exemple car le mode plein écran fait disparaître la « Barre Caption » et que c'est justement dans celle-ci que nous allons afficher nos FPS. cheeky

      On continue. En dessous de la remarque // TODO : utiliser this.Content pour charger le contenu de jeu ici, ajoutez l'instruction suivante :

 Background = Content.Load<Texture2D>("background");

     Attention, il s'agit d'un background de dimensions 800 x 600 et si vous l'utilisez avec un mode d'écran que vous aurez défini plus grand, il ne remplira pas totalement votre écran mais ceci n'est qu'à titre d'exemple et ce qui compte est bien sûr de voir notre Framerate affiché, non.... ? wink

    Et pendant que vous y êtes, afin de ne pas l'oublier, vous pouvez faire un clic droit sur CalculeFPSXNAContent(Content) dans la fenêtre de l'explorateur de solutions à droite et ajouter l'élément existant « background.png » à votre projet (ou tout autre background en votre possession et adapté à votre fenêtre...).

     Maintenant, supprimez complètement la fonction Update() et remplacez-la par celle-ci 

               protected override void Update(GameTime gameTime)
              {
                 // Permet la sortie du jeu
                 if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                 this.Exit();

                 // TODO: Ajouter la logique de mise à jour ici
                 millisecondesEcoulees += gameTime.ElapsedGameTime.TotalMilliseconds;

                 if (millisecondesEcoulees > 1000)
                 {
                     millisecondesEcoulees -= 1000;
                     fps = nbFrames;
                     nbFrames = 0;
                 }           

                 base.Update(gameTime);
              }

 

     Supprimez ensuite la fonction Draw() et remplacez-la par celle-ci :

 

               protected override void Draw(GameTime gameTime)
              {
                  GraphicsDevice.Clear(Color.CornflowerBlue);

                  // TODO: Ajoutez le code de dessin ici
                  //Début de fa fonction de dessin
                  spriteBatch.Begin();
                  
                  spriteBatch.Draw(Background, Vector2.Zero, Color.White);

                  nbFrames++;
                  this.Window.Title = "Nb Frames / sec = " + fps;
            
                  //Fin de la fonction de dessin
                  spriteBatch.End();

                  base.Draw(gameTime);
              }

 

     Dans la fonction Update(), après que le nombre de millisecondes est incrémenté de 0 à 1000, la variable millisecondesEcoulees est remise à 0 et le nombre de frames est ajouté à la variable fps avant que la variable nbFrames ne soit elle-même réinitialisée à 0.

     Dans la fonction Draw(), le nombre de frames est incrémenté d'une unité avant que fps ne soit affiché dans la « barre caption » par l'instruction this.Window.Title = "Nb Frames / sec = " + fps;

     Notez qu'en C#, la concaténation se fait à l'aide du signe « + » qui dans notre cas, ajoute une valeur de type int à une chaine de caractères.

     Voilà pour le Frame Rate, sauvez votre projet et compilez. (Fichier -> Enregistrez tout) puis (Déboguer -> Générer la Solution). Pressez ensuite la touche F5, vous obtiendrez la fenêtre suivante (dans le cas d'un mode d'écran inférieur ou égal à 800 x 600 pixels.


     Vous voyez qu'ici le nombre de fps est de 32 car j'utilise l'outil « Capture » de Windows pendant l'exécution du programme mais dès que je ferme la fenêtre de l'outil capture, les fps remontent immédiatement à 60 (quand on sait qu'un jeu vidéo utilise 100 % des ressources de l'Unité Centrale, on voit à quel point une application parallèle peut grandement faire chuter le nombre de frames/sec). angry

 

     2 – Gestion des « Inputs »

     Nous abordons maintenant la gestion des « Inputs » qui va nous permettre de déplacer des sprites dans une fenêtre. Enfin, diront certains, ce n'est pas trop tôt !!... laugh

     Oui mais pour bien comprendre tous les procédés utilisés, nous ne pouvons pas passer à côté de quelques notions importantes, dont nous avons déjà vues certaines jusqu'ici.

 

               a – Déplacement des sprites à l'aide des touches fléchées

     Ici nous parlons de déplacement de sprites (nous verrons l'animation des sprites dans le prochain chapitre).

    Vous pouvez dès à présent faire un copier/coller de notre programme du premier chapitre (AfficheUnSprite) et le renommer « DeplacerSprite ». Nous allons modifier ce projet au fur et à mesure de nos besoins.

     Que voulons nous faire exactement ? Eh bien actuellement, quand nous pressons la touche F5 pour démarrer notre projet, nous voyons deux images statiques, l'une à l'échelle 1:1 et l'autre à l'échelle 2.5:1.

     Nous allons déplacer chacune de nos sorcières, mais pour garder un certain effet de profondeur, nous ferons passer le plus petit sprite derrière le plus grand et le plus grand devant le plus petit (logique si nous voulons obtenir un effet de profondeur réaliste, non...?).

    Ok, préparez votre projet « DeplacerSprite » car nous allons procéder à plusieurs changements. cool

     Supprimez les 10 données de Destination et remplacez-les par celles-ci :

 

        //Données de destination
        public Vector2 spritePosition;
        public Vector2 sprite2Position;
        public Color destColor;
        public float rotationSprite1;
        public float rotationSprite2;
        public float echelleSprite1;
        public float echelleSprite2;
        public float profondeur;
        public SpriteEffects spriteEffetSprite2;
        public SpriteEffects spriteEffetSprite1;

 


     Dans ce projet nous utiliserons deux méthodes surchargées de la classe SpriteBatch, et c'est pourquoi nous scindons les variables rotation, echelle, et spriteEffet en deux nouvelles variables qui représenteront les états de nos deux sorcières.

     En dessous de Color myBackgroundColor; ajoutez les deux variables de type bool suivantes ainsi qu'une variable de type GamePadState :

 

        bool sprite1;
        bool sprite2;

        GamePadState gamepadState;

 


     Nous initialiserons les deux booléens un peu plus tard, ils permettront de faire défiler soit le sprite 1 soit le sprite 2.
     La variable gamepadState de type GamePadState nous permettra de faire défiler ces deux sprites à l'aide du Gamepad.

     Supprimez ensuite la fonction Initialise() et remplacez-la par celle-ci :

 

        protected override void Initialize()
        {
            // TODO: Ajoutez les initialisations ici
            spritePosition.X = 250;
            spritePosition.Y = 150;
            sprite2Position.X = 120;
            sprite2Position.Y = 180;


            myBackgroundColor = new Color(200, 200, 200);
            rotationSprite1 = 0.0f;
            rotationSprite2 = 0.30f;
            
            echelleSprite2 = 2.5f;
            echelleSprite1 = 1.0f;
            localisation = new Rectangle(0, 0, 60, 50);
            origine = Vector2.Zero;
            spriteEffetSprite1 = SpriteEffects.None;
            spriteEffetSprite2 = SpriteEffects.FlipHorizontally;
            profondeur = 1.0f;
            destColor = Color.White;

            base.Initialize();
        }

 

     Dans cette fonction nous faisons les initialisations de nos nouvelles variables, le sprite 1 n'ayant pas de rotation (0.0f), son échelle restant fixe 1:1 (1.0f) et n'a aucun effet spécial à l'initialisation (SpriteEffects.None). Le sprite 2 garde son échelle à 2.5:1, avec un effet renversé horizontal (SpriteEffects.FlipHorizontally).

     Dans la fonction LoadContent(), ajoutez la ligne suivante :

spriteTexture = Content.Load<Texture2D>("monster2");

 

     Le plus gros des modifications va se faire dans la fonction Update(). Supprimez-la et remplacez-la par celle-ci :

 

        protected override void Update(GameTime gameTime)
        {
            // Permet la sortie du jeu
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Ajouter la logique de mise à jour ici
            gamepadState = GamePad.GetState(PlayerIndex.One);

            /* Si l'on presse la touche C du clavier ou le bouton A du gamepad,
               on va débloquer le déplacement du sprite 2 */
            if (Keyboard.GetState().IsKeyDown(Keys.C) || 
               (gamepadState.Buttons.A == ButtonState.Pressed))
            {
                sprite1 = false;
                sprite2 = true;
            }

            //Si l'on presse la touche X du clavier ou le bouton X du gamepad,
              on va débloquer le déplacement du sprite 1
            if (Keyboard.GetState().IsKeyDown(Keys.X) ||
               (gamepadState.Buttons.X == ButtonState.Pressed))
            {
                sprite1 = true;
                sprite2 = false;
            }



            //Déplacement au clavier ou au gamepad du sprite 2 (sorcière à l'échelle 2.5:1)
            if (sprite2)
            {
                if (Keyboard.GetState().IsKeyDown(Keys.Right) ||
                   (gamepadState.DPad.Right == ButtonState.Pressed))
                {
                    spriteEffetSprite2 = SpriteEffects.FlipHorizontally;
                    rotationSprite2 = 0.30f;
                    sprite2Position.X += 2;
                    sprite2Position.Y += 1;                   
                }

                if (Keyboard.GetState().IsKeyDown(Keys.Left) ||
                   (gamepadState.DPad.Left == ButtonState.Pressed))
                {
                    spriteEffetSprite2 = SpriteEffects.None;
                    rotationSprite2 = 0.30f;
                    sprite2Position.X -= 2;
                    sprite2Position.Y -= 1;
                }

                if (Keyboard.GetState().IsKeyDown(Keys.Up) ||
                   (gamepadState.DPad.Up == ButtonState.Pressed))
                {
                    rotationSprite2 = 0.0f;
                    sprite2Position.Y -= 2;
                }

                if (Keyboard.GetState().IsKeyDown(Keys.Down) ||
                   (gamepadState.DPad.Down == ButtonState.Pressed))
                {
                    rotationSprite2 = 0.0f;
                    sprite2Position.Y += 2;
                }
            }

            //Déplacement au clavier ou au gamepad du sprite 1 (sorcière à l'échelle 1:1)
            if (sprite1)
            {
                if (Keyboard.GetState().IsKeyDown(Keys.Right) ||
                   (gamepadState.DPad.Right == ButtonState.Pressed))
                {
                    spriteEffetSprite1 = SpriteEffects.FlipHorizontally;
                    spritePosition.X += 2;
                }

                if (Keyboard.GetState().IsKeyDown(Keys.Left) ||
                   (gamepadState.DPad.Left == ButtonState.Pressed))
                {
                    spriteEffetSprite1 = SpriteEffects.None;
                    spritePosition.X -= 2;
                }

                if (Keyboard.GetState().IsKeyDown(Keys.Up) ||
                   (gamepadState.DPad.Up == ButtonState.Pressed))
                    spritePosition.Y -= 2;                

                if (Keyboard.GetState().IsKeyDown(Keys.Down) ||
                   (gamepadState.DPad.Down == ButtonState.Pressed))
                    spritePosition.Y += 2;
            }

            base.Update(gameTime);
        }

 

     Une dernière chose, supprimez également la fonction protected override void Draw(GameTime gameTime) et remplacez-la par celle-ci :

 

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(myBackgroundColor);

            // TODO: Ajoutez votre code de dessin ici
            spriteBatch.Begin();

            
            spriteBatch.Draw(spriteTexture,
                             spritePosition,
                             localisation,
                             destColor,
                             rotationSprite1,
                             origine,
                             echelleSprite1,
                             spriteEffetSprite1,
                             profondeur);

            spriteBatch.Draw(spriteTexture,
                             sprite2Position,
                             localisation,
                             destColor,
                             rotationSprite2,
                             origine,
                             echelleSprite2,
                             spriteEffetSprite2,
                             profondeur);
            spriteBatch.End();

            base.Draw(gameTime);
        }

 

     La fonction Update() demande quelques explications...

     Premièrement nous allons choisir quel sprite nous allons déplacer sur l'écran. Au départ, l'appui sur les touches fléchées ne donne aucun résultat. Normal pour l'instant... sad

     Pour débloquer le mouvement de l'un ou l'autre sprite à l'aide des touches fléchées, nous avons le choix, soit nous utilisons le clavier du PC soit le gamepad, et nous procèderons comme suit :

     Souvenez-vous des deux variables booléennes sprite1 et sprite2 que nous avons introduites un peu plus haut, c'est ici que nous allons les initialiser. wink

     Supposons que nous décidions de mettre en mouvement le sprite1 (celui à l'échelle 1:1). Pour ce faire nous presserons soit la touche X du clavier du PC ou le bouton X du gamepad. Ceci aura pour effet d'initialiser nos deux booléens (sprite1 = true;) et (sprite2 = false;), pour empêcher le sprite 2 (celui à l'échelle 2.5:1) de se déplacer quand nous mettons le sprite 1 en mouvement. Nous inverserons la valeur des booléens quand nous presserons soit la touche C du clavier du PC ou le bouton A du gamepad pour permettre alors au sprite 2 de se déplacer tout en empêchant tout mouvement du sprite 1.

     Ceci fait, nous déplacerons l'un ou l'autre sprite à l'aide des touches fléchées du clavier ou du gamepad.


     Vous verrez que j'ai modifié quelques variables agissant sur l'effet et la rotation du sprite2 pour permettre une ascension ou une descente en diagonale, ceci pour vous montrer les possibilités qu'offre la classe SpriteBatch.

     Faites d'autres essais sur chacun des sprites ou même sur les deux, vous voyez qu'avec une seule image d'origine (monster2.png) nous pouvons déjà faire quelques figures intéressantes à l'aide d'une méthode surchargée de la classe SpriteBatch. Les images ci-dessous vous montreront les deux effets de profondeurs obtenus. smiley

 


Sprite 2 passe devant Sprite 1 et paraît donc plus rapproché.

 


Sprite 1 passe derrière Sprite 2 et paraît donc plus éloigné.

 

     Ces deux images semblent être les mêmes et en effet elles le sont (dans les deux cas le sprite 2 se trouve devant le sprite 1) mais ce qui est intéressant de voir, ce sont les déplacements de l'un ou l'autre, car lorsqu'ils se croisent un effet de profondeur logique est respecté.

     Le fait que nous respections un certain effet de profondeur n'est pas dû au hasard, non...
    Si vous observez la fonction Draw(), vous remarquerez l'ordre dans lequel les  méthodes spriteBatch.Draw() sont écrites. Vous voyez dans la fonction Draw() que le premier sprite affiché est Sprite 1, de position spritePosition et que celui affiché en second est Sprite 2, de position sprite2Position.
    L'ordre dans lequel vous affichez vos sprites, lors d'un projet quelconque, est donc très important selon l'effet que vous voulez obtenir sur votre écran et vous devrez toujours garder ceci à l'esprit. enlightened

                b – Un exemple de sprite qui se déplace seul

     Pour terminer ce tutoriel sur le déplacement des sprites (avant de bientôt retrouver nos sorcières...) nous allons maintenant voir le cas d'un sprite se déplaçant seul et ajuster sa vitesse de déplacement.

     De nouveau, créez un nouveau projet et appelez-le « SpriteMoveAlone ». Nous allons de suite ajouter le code nécessaire à la réalisation de ce projet.

     Dans la classe Game1(), en dessous de SpriteBatch spriteBatch; , ajoutez les variables suivantes :

 

        Texture2D sprite;

        Vector2 arwell1Position;
        Vector2 arwell2Position;
        Vector2 arwell3Position;


     Supprimez la fonction LoadContent() et remplacez-la par celle-ci :

 

         

         // LoadContent est appelée une fois par partie et charge le contenu du projet
        protected override void LoadContent()
        {
            // Crée un nouveau spriteBatch pour le dessin des textures
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO : utiliser this.Content pour charger le contenu de jeu ici
            sprite = Content.Load < Texture2D > ("ArwellPirate2");
        }

 

Attention, pour éviter que Geshi ne prenne <Texture2D> pour une balise HTML, j'ai dû rajouter des espaces. Enlevez-les si vous faites du copier/coller ! wink

     Et pendant que vous y êtes, afin de ne pas l'oublier, vous pouvez faire un clic droit sur SpriteMoveAloneContent(Content) dans la fenêtre de l'explorateur de solutions à droite et ajouter l'élément existant « ArwellPirate2.png » à votre projet. Tâchez de vous rappeler de charger vos assets pour vos prochains tutos hein... ? wink

     Supprimez ensuite la fonction Update() et remplacez-la par celle-ci :

 

           protected override void Update(GameTime gameTime)
         {
            // Permet la sortie du jeu
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Ajouter la logique de mise à jour ici
            int yPosition = 480 - gameTime.TotalGameTime.Milliseconds/2;
            int y2Position = 0 + gameTime.TotalGameTime.Milliseconds/2;
            int x3Position = gameTime.TotalGameTime.Milliseconds;
            arwell1Position = new Vector2(300, yPosition);
            arwell2Position = new Vector2(350, y2Position);
            arwell3Position = new Vector2(x3Position, 240);
            base.Update(gameTime);
         }

 

      Et enfin supprimez la fonction Draw() que vous remplacerez par celle-ci :

          protected override void Draw(GameTime gameTime)
         {
            GraphicsDevice.Clear(Color.White);

            // TODO: Ajouter le code du dessin ici
            spriteBatch.Begin();
            //spriteBatch.Draw(background, Vector2.Zero, Color.White);
            spriteBatch.Draw(sprite, arwell1Position, Color.White);
            spriteBatch.Draw(sprite, arwell2Position, Color.White);
            spriteBatch.Draw(sprite, arwell3Position, Color.White);
            spriteBatch.End();
            base.Draw(gameTime);
        }

 

     Nous voyons que tout se fait dans la fonction Update(). Nous initialisons 3 variables donnant chacune l'origine et la direction d'un sprite.

     En effet, la position d'Arwell1 a son origine en (300, yPosition), avec yPosition = 480 – gameTime.TotalGameTime.Milliseconds/2; , c'est-à-dire le bas de la fenêtre (480), de direction ascendante (de 480 vers 0) et dont le temps de parcours du sprite est donné par gameTime.TotalGameTime.Milliseconds/2.


      La position d'Arwell2 est semblable à celle d'Arwell1. Son origine est en (350, yPosition) , avec yPosition = 0 + gameTime.TotalGameTime.Milliseconds/2; , c'est-à-dire le haut de la fenêtre (0), de direction descendante (de 0 vers 480) et dont le temps de parcours du sprite est également donné par gameTime.TotalGameTime.Milliseconds/2.


     Ces deux sprites vont donc à la même vitesse, se déplacent verticalement, mais dans des directions opposées.

     Arwell3, quant à elle, se déplace horizontalement et à une vitesse différente.

     La position de départ de Arwell3 a une abscisse égale à 0. Celle-ci est rapidement incrémentée par gameTime.TotalGameTime.Milliseconds; , c'est-à-dire le temps qu s'écoule pendant une boucle de jeu, son ordonnée étant donnée constante soit 240.

     Voilà, sauvez votre projet et compilez (Fichier -> Enregistrez tout puis Déboguer -> Générer la Solution). Pressez ensuite la touche F5, vous obtiendrez une fenêtre du genre de la suivante avec nos 3 pirates se déplaçant dans le sens des flèches. cool

 

                        c – Un autre exemple de sprite qui se déplace seul :
                               Balle bondissante en collision avec les bords de la fenêtre

     Nous n'avons pas encore parlé des collisions avec les côtés d'une fenêtre, nous allons le faire ici dans un exemple de balle bondissant sur votre écran.

     Créez un nouveau projet et nommez-le « BoundingBall ». Comme à l'habitude, nous allons modifier ce projet suivant nos besoins.

     A l’intérieur de la classe public class Game1 : Microsoft.Xna.Framework.Game , à la suite de SpriteBatch spriteBatch; , vous ajouterez les variables suivantes :
 

         private Texture2D balle;
        private Vector2 positionBalle;

        // La direction prise par la balle
        private Vector2 directionBalle;

        // Variable de la vitesse de déplacement de la balle
        private float vitesseBalle = 0.1f;

        // La largeur et la hauteur de la fenêtre
        private int largeurFenetre;
        private int hauteurFenetre;

 

     Vous voyez qu'il nous faut donner une direction à la balle, de même nous initialisons sa vitesse avec une variable de type float.

          protected override void Initialize()
         {
            // TODO: Ajourez la logique d'initialisation ici
            graphics.PreferredBackBufferWidth = 800;
            graphics.PreferredBackBufferHeight = 600;
            graphics.ApplyChanges();
           
            positionBalle = Vector2.Zero;
            directionBalle = Vector2.Normalize(Vector2.one);
          
            // On initialise les dimensions de la fenêtre
            largeurFenetre = Window.ClientBounds.Width;
            hauteurFenetre = Window.ClientBounds.Height;

            base.Initialize();
         }

 

     Dans cette fonction, nous donnons des dimensions à notre fenêtre, nous positionnons la balle en (0, 0), et lui donnons une direction à l'aide de Vector2.One , qui est identique à l'écriture Vector2(1, 1). La balle va donc se déplacer en diagonale dans une fenêtre de largeur et de hauteur données (800 x 600) qui sont connues dans Window.ClientBounds.Width et dans Window.ClientBounds.Height. Vous pouvez utiliser vos propres dimensions en changeant simplement les valeurs 800 et 600 dans votre projet.

     Dans la fonction LoadContent(), ajoutez la ligne suivante :

        balle = Content.Load<Texture2D>("ball");

     Supprimez ensuite la fonction Update() et remplacez-la par celle-ci :

        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
            // On déplace la balle en lui ajoutant la variable de direction
            positionBalle += directionBalle;
            positionBalle += directionBalle * vitesseBalle *
                               (float)gameTime.ElapsedGameTime.TotalMilliseconds;
            
            // Ici on teste si la balle se déplace vers la gauche, si c'est le cas
            // on vérifie qu'elle ne sort pas de l'écran par la gauche.
            // Même chose pour le déplacement vers la droite
            if ((directionBalle.X < 0 && positionBalle.X < = 0)
                 || (directionBalle.X > 0 && positionBalle.X +
                                                 balle.Width > = largeurFenetre))
            {
                // Si on est dans un des deux cas, on inverse le déplacement sur les 
                // abscices
                directionBalle.X = - directionBalle.X;
            }

            // On fait la même opération mais pour le haut/bas
            if ((directionBalle.Y < 0 && positionBalle.Y < = 0)
                 || (directionBalle.Y > 0 && positionBalle.Y + balle.Height > =  
                                                                   hauteurFenetre))
            {
                // Si c'est le cas, on inverse le déplacement sur les ordonnées
                directionBalle.Y = (- directionBalle.Y);
            }

            positionBalle += directionBalle * 
                                        (float)gameTime.ElapsedGameTime.TotalMilliseconds;
            base.Update(gameTime);
        }

Attention ! Pour éviter que Geshi ne prenne >= et ça <= pour des balises HTML, j'ai dû rajouter un espace dans le code source ci-dessus. Enlevez ces espaces, si vous faites du copier/coller. wink

     Cette fonction se comprend aisément. On déplace la balle en ajoutant une variable de direction et on teste les cas où la balle entre en collision avec les paires de côtés gauche/droit et haut/bas, auquel cas on inverse son sens de direction.

     Pour terminer, remplacez la fonction Draw() par celle-ci :

 

           protected override void Draw(GameTime gameTime)
          {
            
            // TODO: Ajoutez le code du dessin ici
            GraphicsDevice.Clear(Color.Black);
            spriteBatch.Begin();

            spriteBatch.Draw(balle, positionBalle, Color.White);
            
            spriteBatch.End();

            base.Draw(gameTime);
          }

     Voilà, il n'y a rien à ajouter en commentaire à cette fonction que vous n'ayez déjà appris.

     Sauvez votre projet et compilez. (Fichier -> Enregistrez tout puis Déboguer -> Générer la Solution). Pressez ensuite la touche F5, vous obtiendrez une fenêtre qui ressemble à ceci :

 

       Un mini jeu : La chasse aux sorcières

                Mise en oeuvre des classes Random et Spritefont

     Voici enfin notre mini jeu de « Chasse aux sorcières » que nous pouvons réaliser uniquement avec ce que nous avons appris jusqu'ici et qui ne contient... qu'un seul sprite... ! Donc très minimaliste mais intéressant à étudier.

Bien, comme d'habitude, créez un nouveau projet. Vous le nommerez « Chassons_la_Sorciere » et nous allons immédiatement modifier son contenu. smiley

     Avant toute chose, et comme nous allons utiliser une police de caractères dans notre jeu, nous devons réaliser une opération que nous n'avons jamais faite jusqu'ici.

     Dans l'explorateur de solution de votre projet, faites un clic droit sur Chassons_la_SorciereContent(Content). Cliquez ensuite sur Ajouter -> Nouvel élément.

     Ceci aura pour effet d'ouvrir la fenêtre suivante :

     Dans la fenêtre du milieu, cliquez sur A Sprite Font.
     Dans le bas de la fenêtre, remplacez SpriteFont1.spritefont par myfont (tapez simplement myfont).
     Cliquez sur Ajouter. Dans le Content de l'explorateur de solutions vous allez voir s'ajouter le fichier myFont.spritefont qui va s'ouvrir en un fichier xml dans la fenêtre de gauche de votre projet.

     En parcourant ce fichier vous allez voir la ligne <FontName>Segoe UI Mono</FontName>.
     Il s'agit d'un nom de police de caractères, ajouté automatiquement par Xna et compris entre les balises <FontName> et </FontName>.
     Je l'ai laissé tel quel mais il est bien entendu que vous pouvez le modifier par le nom d'une autre police se trouvant sur votre PC (attention cependant aux copyrights, prenez plutôt une police libre de droit).

     Un peu plus bas, vous voyez la taille de la police, <Size>14</Size>. Remplacez le nombre 14 par 18 et ne touchez à rien d'autre pour ce projet. Sauvez le tout une première fois : Fichier > Enregistrer tout et faites de nouveau apparaître votre projet en double-cliquant sur Game1.cs dans l'explorateur de solutions. Voilà une bonne chose de faite...


     Maintenant, dans la classe Game1(), en dessous de SpriteBatch spriteBatch; , entrez toutes les données suivantes :

 

         //Données sources
        private Rectangle localisation;
        private Vector2 origine;

        //Données de destination
        public Vector2 spritePosition;
        int choixSprite = 0;
        public Color destColor;
        public float rotation;
        public float echelle;
        public float profondeur;
        public SpriteEffects spriteEffect;

        // Une position aléatoire
        Random rand = new Random();
        
        // Nos textures et surface
        Texture2D textureSorciere;
        Texture2D background;
        Rectangle sorciere;
       
        // Une police de caractères
        private SpriteFont font;
        
        // Gestion de l'affichage
        private int playerScore = 0;
        double nbOccurences = 0;
        
        // Gestion du temps 
        float tempsRestant = 0.0f;
        const float tempsAffichage = 0.80f;

 

     Les nouveaux types de données dont nous n'avons pas encore parlé sont Random et SpriteFont, les autres étant connus.

     Le type Random permet d'initialiser des variables pour générer des nombres ou autres objets aléatoires, le type SpriteFont, quant à lui représente des variable s'occupant de la gestion des polices de caractères (Fonts), nous venons de le découvrir plus haut.

     Nous allons voir l'utilité de toutes ces variables en analysant nos différentes fonctions. Bien, maintenant comme à notre habitude, supprimez la fonction Initialize() et remplacez-la par celle-ci :

 

          protected override void Initialize()
         {
            // TODO: Ajoutez la logique d'initialisation ici
            //graphics.PreferredBackBufferWidth = 1024;
            //graphics.PreferredBackBufferHeight = 768;
            //graphics.ApplyChanges();

            rotation = 0.0f;
            echelle = 1.0f;
            localisation = new Rectangle(0, 0, 60, 50);
            origine = Vector2.Zero;
            spriteEffect = SpriteEffects.None;
            profondeur = 1.0f;
            destColor = Color.White;

            this.IsMouseVisible = true;
          
            base.Initialize();
         }

 

     Nous avons déjà parlé des différentes variables que nous retrouvons dans cette fonction. Notons cependant l'instruction this.IsMouseVisible = true; , qui permet de rendre la souris visible à l'écran.

     Supprimez ensuite la fonction LoadContent() et remplacez-la par celle-ci :

 

         protected override void LoadContent()
         {
            // Crée un nouveau SpriteBatch, avec lequel nous pouvons dessiner des textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: utilisez this.Content pour charger tous vos médias ici
            textureSorciere = Content.Load < Texture2D > ("monster2");
            background = Content.Load < Texture2D > ("wallpaperhalloween2");
            font = Content.Load < SpriteFont > ("myFont");
         }

Attention, pour éviter que Geshi ne prenne <Texture2D> pour une balise HTML, j'ai dû rajouter des espaces. Enlevez-les si vous faites du copier/coller ! wink 

      Nous allons pour la première fois ajouter un background à notre projet. Et comme nous parlons de sorcières, pourquoi ne pas placer le « wallpaperhalloween2 » que Jay nous a si gentiment offert... ?

     Ajoutez donc ce fichier ainsi que le fichier monster2.png dans le Content de votre projet (Dois-je vous rappeler que vous les aurez auparavant extraits dans le dossier Chassons_la_SorciereContent, je ne dois pas hein...?). La police myFont ayant, quant à elle déjà été chargée automatiquement lors de l'opération que nous avons réalisée auparavant.

     Nous allons maintenant écrire la fonction Update() de notre projet, c'est là que se déroule toute la logique du jeu.

     Bien, supprimez la fonction Update() et remplacez-la par celle-ci :

 

        protected override void Update(GameTime gameTime)
        {
            // Permet la sortie du jeu
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            MouseState mouse = Mouse.GetState();

            // TODO: Ajoutez la logique de mise à jour ici
            if (tempsRestant == 0.0f)
            {
                spritePosition.X = rand.Next(0, this.Window.ClientBounds.Width - 60);
                spritePosition.Y = rand.Next(0, this.Window.ClientBounds.Height - 50);
           
                sorciere = new Rectangle((int)spritePosition.X, 
                                                       (int)spritePosition.Y, 60, 50);
                choixSprite = rand.Next(1, 4);
                
                switch(choixSprite)
                {
                    case 1 :
                        echelle = 0.5f;
                        spriteEffect = SpriteEffects.None;
                        rotation = 0.0f;
                        break;
                    
                    case 2 :
                        echelle = 1.0f;
                        spriteEffect = SpriteEffects.FlipHorizontally;
                        rotation = 0.0f;
                        break;

                    case 3 :
                        echelle = 1.5f;
                        spriteEffect = SpriteEffects.None;
                        rotation = 0.30f;
                        break;

                    default :
                        break;
                }

                if (nbOccurences < 25)
                    nbOccurences++;
                tempsRestant = tempsAffichage;
            }

            if ((mouse.LeftButton == ButtonState.Pressed) &&
                                               (sorciere.Contains(mouse.X, mouse.Y)))
            {
                playerScore++;
                tempsRestant = 0.0f;
            }

            if (nbOccurences < 25)
            {
                tempsRestant = MathHelper.Max(0, tempsRestant -
                                        (float)gameTime.ElapsedGameTime.TotalSeconds);
            }
          
            base.Update(gameTime);

        }

 

     Nous allons voir de suite ce dont il s'agit mais terminons-en d'abord avec notre dernière fonction.

     Supprimez la fonction Draw() et remplacez-la par celle-ci :

 

         protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.White);

            // TODO: Add your drawing code here

            spriteBatch.Begin();

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

            if (nbOccurences < 25)
            {
                spriteBatch.Draw(textureSorciere,
                spritePosition,
                localisation,
                destColor,
                rotation,
                origine,
                echelle,
                spriteEffect,
                profondeur);

                spriteBatch.DrawString(font, "Score: " + playerScore.ToString() + 
                   " / " + nbOccurences.ToString(), new Vector2(15, 15), Color.Black);
            }
            else
               spriteBatch.DrawString(font, "Score: " + playerScore.ToString() + 
                        " / " + nbOccurences.ToString(), new Vector2(15, 15), Color.Black);

            spriteBatch.End();

            base.Draw(gameTime);

        }

 

            La logique du jeu : fonction Update()

     Voici, sommairement expliqué, ce que fait ce jeu, les explications détaillées de la fonction Update vont suivre ensuite :

     Dès l'ouverture de la fenêtre de jeu, celui-ci démarre très vite. 25 sorcières de dimensions et de positionnements aléatoires vont s'afficher les unes après les autres pendant un temps très court sur la fenêtre du jeu. Le but est d'essayer de cliquer sur chacune d'entre-elles à l'aide de la souris, afin de les faire disparaître. Le score obtenu sera affiché en haut à gauche de l'écran ainsi que le nombre de sorcières déjà affichées. Vous voyez que c'est très minimaliste mais la fonction Update() est très intéressante à étudier d'un point de vue du code. Si vous êtes là c'est que vous voulez apprendre.... non... ? wink

     Bien, voyons cela. En haut de la fonction vous voyez la ligne suivante :

 MouseState mouse = Mouse.GetState();

     Nous déclarons ici et initialisons une variable mouse, de type MouseState , et qui nous permettra à chaque déplacement de la souris, de retrouver la position de celle-ci sur l'écran. Logique si nous voulons toucher un sorcière hein... ? cheeky

     Nous entrons ensuite dans une boucle qui teste si la variable tempsRestant est égale à 0. Cette variable, de type float et initialisée tout en haut de notre programme, calcule le temps qu'il reste à une sorcière pendant lequel elle sera affichée à l'écran.

     Nous voyons que nous avons défini le temps d'affichage d'un sprite à l'écran à 0.8f (const float tempsAffichage déclarée en début de programme). Vous pourrez modifier ce temps si vous voulez mais pour garder une difficulté raisonnable, laissez-le compris entre 0.7f et 0.9f.

     Nous donnons ensuite des valeurs aléatoires aux coordonnées de la variable spritePosition qui affichera un sprite à l'intérieur d'une zone égale à notre fenêtre mais à laquelle on soustrait les dimensions d'un sprite 1:1 afin que son affichage ne déborde pas de notre fenêtre (this.Window.ClientBounds.Width – 60 et this.Window.ClientBounds.Height – 50).

     Nous instancions ensuite la variable sorcière de type Rectangle afin de délimiter les surfaces dans lesquelles on cliquera pour pouvoir la « tuer ». Ces surfaces prennent alors les coordonnées aléatoires que nous venons de définir ci-dessus et qui seront attribuées à chacune des sorcières affichées.
     Remarquons à ce sujet que nous devons effectuer un (cast) devant l'ordonnée et l’abscisse de la variable spritePosition sinon le compilateur nous signale une erreur disant « Impossible de convertir un float en int ».

     Nous voulons afficher aléatoirement trois types de sprites, c'est pourquoi nous donnons également une valeur aléatoire à la variable choixSprite par l'instruction choixSprite = rand.Next(1, 4); , soit les valeurs 1, 2 ou 3, le dernier chiffre n'étant pas pris en compte dans cette recherche aléatoire.

     Ensuite, à l'aide d'une expression switch/case, selon la valeur aléatoire attribuée, nous affectons des valeurs différentes aux variables echelle, rotation et spriteEffet afin de différencier l'apparence de nos sorcières affichées à l'écran.

     A la sortie de notre boucle switch, après avoir incrémenté nbOccurences, nous réinitialisons la variable tempsRestant avec tempsAffichage et ce, tant que nbOccurences reste inférieur à 25 (nbOccurences étant la variable qui incrémente de nombre d’occurrences, soit le nombre d'apparitions de sorcières à l'écran).

     Nous vérifions ensuite si nous avons cliqué sur une sorcière : (sorciere.Contains(mouse.X, mouse.Y)) et si oui nous incrémentons le score du joueur d'une unité et nous mettons le temps restant à cette sorcière à 0.

     Ensuite, toujours en fonction du nombre d’occurrences, nous réinitialisons le temps restant par un calcul qui se fait à l'aide d'une classe spéciale de Xna, dénommée MathHelper et dont l’expression complète est :

tempsRestant = MathHelper.Max(0, tempsRestant-(float)gameTime.ElapsedGameTime.TotalSeconds)

 

              L'affichage du jeu : fonction Draw()

      Cette fonction n'a rien de compliqué : elle affiche simplement 25 sorcières les unes à la suite des autres, de façon aléatoire et avec des paramètres différents. Elle affiche en outre le score en haut à gauche de l'écran.

    En C#, la fonction ToString() transforme une variable quelconque en chaîne de caractères. Dans ce cas, playerScore.ToString() transforme le score (qui est un entier) en chaîne. Et l'expression "Score: " + playerScore.ToString() + " / " + nbOccurences.ToString() ne fait plus qu'une seule chaîne de caractères après concaténation.

     A la sortie de la boucle, vous remarquerez que j'ai réécrit la fonction DrawString() une seconde fois. En effet, nous ne devons pas perdre de vue que tout notre code est écrit dans une seule classe, la classe Game1 et pour que le score reste affiché dès la disparition de la 25eme sorcière, cette fonction a dû être ré affichée à cet endroit.

     Voila pour ce mini-jeu. Sauvegardez votre projet, et compilez-le ( Fichier -> Enregistrer tout puis Déboguer -> générer la solution) . Pressez ensuite F5, vous obtiendrez une fenêtre de ce style :

     Soit cette autre capture :

     Voilà pour ce troisième chapitre. Il est assez long, mais il m' était difficile de le faire plus court sans perdre quelque information importante. wink

     Quoi qu'il en soit j'espère avoir été le plus complet possible, surtout pour les débutants. Nos prochains chapitres porteront sur l'animation des sprites et les principes d'accélération appliquée à l'animation.

     A bientôt ! cool
 

 

Connexion

CoalaWeb Traffic

Today205
Yesterday182
This week681
This month4497
Total1738712

28/03/24