Programmation graphique

Chapitre 8 : La Caméra - 2ème partie

 

Tutoriel présenté par : Robert Gillard (Gondulzak)
Publication : 22 mars 2014
Dernière mise à jour : 22 novembre 2015

 

      Préliminaires

     Dans le chapitre 7 : La Caméra – première partie, nous avons vu comment faire défiler une map dans les quatre directions (haut, bas, gauche, droite). Nous avons utilisé pour ce faire, un tableau d'entiers dans lequel nous avons chargé des sprites à partir d'une List<Texture2D>.

   Dans ce chapitre, nous allons rendre notre projet précédent bien plus efficace au niveau du rendu. cool
   En effet, si vous jetez un coup d'oeil à la fonction Draw() du projet DefilMap, vous vous apercevrez que :
                      1 – Nous dessinons en entier toutes les tiles de notre map.
                      2 – Nous créons en outre un objet Rectangle pour chaque tile dessinée.

 

   Bien entendu, pour une petite carte comme celle dessinée dans le projet DefilMap, cela ne cause aucun problème mais imaginez-vous le travail de la fonction Draw() pour redessiner des tableaux de maps de 300 à 400 tiles ou bien plus, chaque fois que l'on déplace la caméra de la largeur (ou hauteur) d'une tile dans l'une des quatre directions surprise. Il faut savoir que la création d'autant d'objets demande pas mal d'allocations mémoire et que le « ménage » est fait par le « garbage collector » à chaque fois que ces objets ne sont plus nécessaires. indecision

   Nous devons aussi savoir que le déplacement d'une map en diagonale par la pression simultanée de deux touches fléchées sera plus rapide qu'un déplacement réalisé à l'aide d'une seule touche fléchée permettant un déplacement horizontal ou vertical. Il serait donc également utile de transformer notre code pour donner une vitesse égale à ces déplacements. wink

   Nous allons voir comment remédier à ces divers inconvénients mais nous allons premièrement créer une classe Camera à notre projet.

 


    1 - Création d'une classe «Camera»


   Créons donc un nouveau projet que nous nommerons DefilMap2. Ne vous préoccupez pas du fichier Game1.cs et laissez-le tel quel pour l'instant. Nous allons y revenir par la suite. wink

   Nous allons maintenant créer la classe « Camera ». Pour ce faire, nous utiliserons la même technique que celle employée dans le chapitre 5. Pressez les touches SHIFT + ALT + C, cliquez sur « classe » dans la fenêtre centrale et entrez le nom « Camera.cs » dans la zone de saisie du bas puis cliquez ensuite sur Ajouter.

   Vous obtenez alors une nouvelle fenêtre vide, dans laquelle vous entrerez le code suivant que nous allons expliquer par la suite : 

 

  
// Projet Defilmap2
// Camera.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace DefilMap2
{
    class Camera
    {
        Vector2 position;
        float speed;

        public Camera()
        {
            this.position = new Vector2();
            this.speed = 4.0f;
        }

        public float Speed
        {
            get { return speed; }
            set
            {
                speed = MathHelper.Clamp(value, 0.5f, 5.0f);
            }
        }

        public Vector2 Position
        {
            get { return position; }

            set
            {
                position.X = MathHelper.Clamp(value.X, 
                        0, 
                        Game1.MapWidthInPixels - Game1.ScreenWidth);
                position.Y = MathHelper.Clamp(value.Y, 
                        0, 
                        Game1.MapHeightInPixels - Game1.ScreenHeight);
            }
        }
    }
}

 

   Comme vous pouvez le constater, la classe Camera n'est pas très longue mais demande plusieurs explications. Alors maintenant, tâchez d'être très attentifs, nous abordons quelques nouveaux concepts... cheeky

   Jetons d'abord un coup d'oeil aux variables déclarées au sein de la classe caméra (Vector2 position et float speed). Celles-ci vont respectivement représenter la position et la vitesse de notre caméra.

   Vous voyez également que la Propriété public Vector2 Position fait intervenir les accesseurs de lecture (get) et d'initialisation (set) se référant à la classe Game1 (nous avons déjà parlé des accesseurs lors de notre 5ème chapitre mais retenons simplement que les Propriétés initiées dans une classe permettent d'accéder soit en lecture (get) soit en écriture (set) à des variables dont nous pourrions avoir besoin dans une autre classe et c'est le cas ici).

   En effet, les propriétés MapWidthInPixels, MapHeightInPixels, ScreenWidth et ScreenHeight que nous allons implémenter dans Game1.cs permettent d'accéder aux variables mapWidthInPixel, mapHeightInPixels, screenWidth et screenHeight que nous déclarerons statiques dans notre classe Game1, d'où la référence à la classe Game1 de ces propriétés. wink

 

   Et puisque nous avons commencé par éclaircir quelques élément de la propriété Position, voyons maintenant le rôle de ses accesseurs cool :

   Nous voyons que get() ne fait que retourner la position de la caméra et que l'accesseur set() a pour mission de bloquer la caméra en lieu et place de la fonction LockCamera() que nous avions introduite dans le chapitre précédent mais qui n'a plus de raison d'être dans ce nouveau projet. En effet, la variable position de type Vector2 va stocker la position de notre caméra à chaque instant et la méthode Clamp de la Classe MathHelper va se charger de bloquer notre caméra en passant la valeur 0 comme valeur mini à position.X et comme valeur maxi la longueur de la map en pixels moins la largeur de l'écran.

   Et nous faisons de même pour la variable position.Y qui prendra 0 comme valeur mini et la hauteur de la map en pixels moins la hauteur de l'écran comme valeur maxi.

   Le constructeur de la classe Camera applique, quant à lui, le type Vector2 à la variable position et initialise la vitesse de la caméra à 4.0f.

 

   Voyons maintenant la propriété :  public float Speed: 

   Celle-ci retourne la vitesse de la caméra (return speed) tandis que l'accesseur set utilise la méthode MathHelper.Clamp pour donner des vitesses mini et maxi à la caméra parce que celle-ci ne peut pas rencontrer des valeurs nulles ou négatives. Nous avons donc choisi les valeurs arbitraires de 0.5f et 5.0f comme limites en tenant compte que, vu la valeur de notre vitesse initiale = 4.0f, ces valeurs doivent être telles que 0.5f < 4.0f < 5.0f .

 

   Voilà, abondamment commentée je pense, l'implémentation de notre classe Camera. Nous allons maintenant pouvoir passer à la classe Game1 et aux modifications de son implémentation. wink

   Pour plus de facilités, effacez complètement le code de la classe Game1, initié automatiquement par XNA lors de la création du projet DefilMap2 et remplacez-le par celui-ci. Vous verrez qu'il y a quand-même quelques différences importantes par rapport à la classe Game1 du projet DefilMap du chapitre précédent. Et à l'intérieur de la fenêtre vide représentant notre classe Game1 vous pouvez y placer le code suivant :  

 

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
 
namespace DefilMap2
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
 
KeyboardState currentState;
 
Camera camera = new Camera();
 
Vector2 direction;
 
List<Texture2D> tiles = new List<Texture2D>();
 
static int tileWidth = 64;
static int tileHeight = 64;
 
int tileMapWidth;
int tileMapHeight;
 
static int screenWidth;
static int screenHeight;
 
static int mapWidthInPixels;
static int mapHeightInPixels;
 
int[,] map =
{
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,0,0,0},
{0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,0,0,3,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0},
{0,0,0,0,0,4,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0},
{0,0,0,0,0,3,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,1,1,2,2,2,2,2,2,2,1},
{0,0,0,0,0,3,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,1,2,2,2,2,2,2,2,2,2,2},
{0,0,0,0,0,3,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2},
{1,1,1,1,1,1,8,8,1,1,1,1,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2},
{2,2,2,2,2,2,9,9,2,2,2,2,1,0,0,0,0,5,0,0,0,2,2,2,2,2,2,2,2,2,2,2},
{2,2,2,2,2,2,9,9,2,2,2,2,2,1,1,1,1,1,8,8,8,2,2,2,2,2,2,2,2,2,2,2},
{2,2,2,2,2,2,9,9,2,2,2,2,2,2,2,2,2,2,9,9,9,2,2,2,2,2,2,2,2,2,2,2},
{2,2,2,2,2,2,9,9,2,2,2,2,2,2,2,2,2,2,9,9,9,2,2,2,2,2,2,2,2,2,2,2},
{2,2,2,2,2,2,9,9,2,2,2,2,2,2,2,2,2,2,9,9,9,2,2,2,2,2,2,2,2,2,2,2},
{2,2,2,2,2,2,9,9,2,2,2,2,2,2,2,2,2,2,9,9,9,2,2,2,2,2,2,2,2,2,2,2},
};
 
 
public static int ScreenWidth
{
   get { return screenWidth; }
}
 
public static int ScreenHeight
{
   get { return screenHeight; }
}
 
public static int MapWidthInPixels
{
   get { return mapWidthInPixels; }
}
 
public static int MapHeightInPixels
{
   get { return mapHeightInPixels; }
}
 
public Game1()
{
   graphics = new GraphicsDeviceManager(this);
   Content.RootDirectory = "Content";
}
 
protected override void Initialize()
{
   tileMapWidth = map.GetLength(1);
   tileMapHeight = map.GetLength(0);
 
   mapWidthInPixels = tileMapWidth * tileWidth;
   mapHeightInPixels = tileMapHeight * tileHeight;
 
   screenWidth = GraphicsDevice.Viewport.Width;
   screenHeight = GraphicsDevice.Viewport.Height;
 
   base.Initialize();
}
 
 
protected override void LoadContent()
{
   spriteBatch = new SpriteBatch(GraphicsDevice);
 
   tiles.Add(Content.Load<Texture2D>("Ciel"));
   tiles.Add(Content.Load<Texture2D>("Sol"));
   tiles.Add(Content.Load<Texture2D>("Mur2"));
   tiles.Add(Content.Load<Texture2D>("Tronc"));
   tiles.Add(Content.Load<Texture2D>("Feuillage"));
   tiles.Add(Content.Load<Texture2D>("Ressort"));
   tiles.Add(Content.Load<Texture2D>("Etoile"));
   tiles.Add(Content.Load<Texture2D>("Nuage"));
   tiles.Add(Content.Load<Texture2D>("EauBasse"));
   tiles.Add(Content.Load<Texture2D>("Eau"));
}
 
protected override void UnloadContent()
{
   // TODO: Déchargez tout le contenu non-manager ici
}
 
 
protected override void Update(GameTime gameTime)
{
   currentState = Keyboard.GetState();
 
   if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
      this.Exit();
   direction = Vector2.Zero;
 
   if (currentState.IsKeyDown(Keys.Up))
       ScrollUp();
   if (currentState.IsKeyDown(Keys.Left))
       ScrollLeft();
   if (currentState.IsKeyDown(Keys.Down))
       ScrollDown();
   if (currentState.IsKeyDown(Keys.Right))
       ScrollRight();
 
   if (direction != Vector2.Zero)
   {
       direction.Normalize();
       camera.Position += direction * camera.Speed;
   }
 
   base.Update(gameTime);
}
 
private void ScrollUp()
{
   direction.Y = -1;
}
 
private void ScrollRight()
{
   direction.X = 1;
}
 
private void ScrollDown()
{
   direction.Y = 1;
}
 
private void ScrollLeft()
{
   direction.X = -1;
}
 
protected override void Draw(GameTime gameTime)
{
   GraphicsDevice.Clear(Color.CornflowerBlue);
 
   spriteBatch.Begin();
 
   for (int y = 0; y < map.GetLength(0); y++)
   {
       for (int x = 0; x < map.GetLength(1); x++)
       {
           spriteBatch.Draw(tiles[map[y, x]],
              new Rectangle(x * tileWidth - (int)camera.Position.X,
              y * tileHeight - (int)camera.Position.Y,
              tileWidth,
               tileHeight),
                Color.White);
        }
     }
 
     base.Draw(gameTime);
     spriteBatch.End();
}
 
}
}

 

   Déclarations et initialisations

  Voyons ce qu'il y a de neuf dans le fichier Game1.cs du projet DefilMap2. Nous créons premièrement une instance de la classe Camera par l'instruction Camera camera = new Camera() afin d'avoir accès à certaines de ses variables. Nous déclarons ensuite une variable direction de type Vector2 qui donnera une direction à notre caméra et qui, nous allons le voir, va surtout permettre d'avoir des vitesses égales aussi bien dans les déplacement en diagonale que dans les déplacements horizontaux ou verticaux. wink

   Remarquons également que nous donnons des valeurs de 64 pixels aux variables tileWidth et tileHeight afin d'agrandir notre zone de déplacement pour mieux se rendre compte de la similitude des vitesses lors des divers déplacements de la caméra. Bien entendu ce changement n'est effectué que dans un but démonstratif et vous pourrez remettre des valeurs de 32 pixels à ces variables si vous le désirez. smiley


   Nous déclarons ensuite les variables tileMapWidth et tileMapHeight qui nous donneront respectivement les largeur et hauteur de la map en nombre de tiles. Nous déclarons ensuite les variables statiques auxquelles nous avons fait référence dans la classe Camera. Il s'agit des variables screenWidth et screenHeight qui vont nous donner les dimensions de l'écran ainsi que les variables mapWidthInPixels et mapHeightInPixels qui représenteront les dimensions de l'écran en pixels.

   Nous écrivons ensuite les Références qui vont retourner ces quatre variables statiques et qui vont nous permettre d'y accéder depuis la classe Camera, comme nous l'avons vu un peu plus haut. wink

   Chacune de ces variables va ensuite être initialisée dans la fonction Initialize() mais nous aurions pu tout aussi bien le faire dans la méthode LoadContent(). Les variables tileMapWidth et tileMapHeight vont respectivement représenter les valeurs X et Y des méthodes GetLength(1) et GetLength(0) dont nous en avons donné la signification dans le chapitre 7. wink

   MapWidthInPixels et mapHeightInPixels seront initialisées avec les valeurs résultant du produit du nombre de tiles par leurs dimensions en pixels :

tileMapWidth * tileWidth et tileMapHeight * tileHeight

 

  Et pour définir les largeur et hauteur : screenWidth et screenHeight, nous utilisons les fonctions graphiques GraphicsDevice.Viewport.Width et GraphicsDevice.Viewport.Height.


     Fonction Update()

   Dans la fonction Update(), nous faisons un test sur la variable « direction ». En effet, si celle-ci est différente de Vector2.Zero, nous devons faire appel à la méthode Normalize() qui donne au vecteur direction une longueur d'une unité, ce qui aura pour effet d'empêcher une vitesse plus rapide de la caméra lors d'un déplacement en diagonale. La position de la caméra est alors incrémentée de la valeur du produit de sa direction par sa vitesse. cool

 

     Fonction Draw()

   Dans la fonction Draw(), nous remplaçons les variables cameraPositionX et cameraPositionY (utilisées dans le projet DefilMap du chapitre 7) par les variables camera.Position.X et camera.Position.Y de notre nouvelle classe et sur lesquelles nous devons effectuer un « cast », car la classe Vector2 utilise le type float.

 

   Sauvegardez le projet, et compilez-le ( Fichier -> Enregistrer tout) puis ( Déboguer -> générer la solution). Pressez ensuite F5. Après avoir pressé sur les touches fléchées, vous obtiendrez une fenêtre de ce genre. 

 

 

   Dans cette fenêtre, les valeurs des variables tileWidth et tileHeight ont été portées à 64 pixels afin d'agrandir la carte et permettre une démonstration plus large des vitesses des divers déplacements de la caméra.


   Voilà pour l'implémentation de notre classe Camera et pour la modification de la vitesse de celle-ci lors de déplacements en diagonale (maintenant nos vitesses de déplacement sont identiques que nous nous déplacions horizontalement, verticalement ou en diagonale). Nous allons maintenant poursuivre ce tutoriel en implémentant une gestion plus efficace du rendu. cool

 

 

     2 – Un rendu plus efficace...

   Au début de la première partie de ce chapitre, je vous ai fait remarquer que l'implémentation de la fonction Draw() du projet DefilMap du chapitre précédent alourdissait considérablement celle-ci et qu'il y avait moyen de la rendre bien plus efficace au niveau du rendu. 

   Eh bien, c'est ce que nous allons faire ici et maintenant ! smiley

   Vous n'avez pas besoin d'initier un nouveau projet pour cette réalisation, nous ferons simplement les quelques modifications dans le projet DefilMap2. Néanmoins, vous pouvez télécharger les 2 projets séparés en suivant le lien ci-dessus (si ce n'est déjà fait, bien sûr). wink

   Afin que la fonction Draw() ne puisse dessiner que les tiles visibles de la fenêtre sans pour autant ne pas avoir à redessiner toute la carte à chaque déplacement de la caméra, nous devons savoir sur quelle tile se trouve la caméra. cheeky

   A cette fin, nous allons créer une nouvelle fonction que nous appellerons VectorToCell. Cette fonction va prendre un Vector2 en paramètre et retournera un Point. Nous avions déjà parlé de la structure Point dans le chapitre 3 lors de l'animation d'un sprite. Nous ajouterons simplement que les propriétés X et Y d'une structure Point sont des entiers contrairement à celles d'un Vector2 qui sont des float.

   Et pour retrouver sur quelle tile se trouve notre vecteur nous allons prendre la valeur X de ce vecteur et la diviser par la largeur de la tile. Nous ferons de même pour sa valeur Y que nous diviserons par la hauteur de la tile et nous effectuerons un « cast » sur les résultats afin de les transformer en entiers.

 

   Dans votre projet DefilMap2, en dessous des fonctions de scrolling, ajoutez la fonction suivante :   

 

  
         private Point VectorToCell(Vector2 vector)
        {
            return new Point(
                            (int)(vector.X / tileWidth),
                            (int)(vector.Y / tileHeight));
        }

 

   Nous avons besoin d'une autre fonction pour effectuer notre rendu. En effet, la fonction LockCamera() ayant disparu, nous avons besoin d'ajouter une tile sur la largeur et la hauteur de notre fenêtre afin que lors du scrolling, nous ne dépassions pas les bords de celle-ci et éviter ainsi de laisser apparaître une partie de notre background. Voici le code de cette fonction que nous appellerons ViewPortVector() et que vous pouvez ajouter à la suite de la fonction VectorToCell().

 

  
        private Vector2 ViewPortVector()
        {
            return new Vector2( screenWidth + tileWidth,
                                screenHeight + tileHeight);
        }

 

   Il ne nous reste plus qu'à dessiner notre rendu mais nous allons le faire dans une fonction séparée que nous allons appeler DrawMap() et que nous intégrerons dans notre fonction Draw().

   Le code de cette dernière ne se résumera dès lors plus qu'à ces quelques lignes :                       

 

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

            DrawMap();

            base.Draw(gameTime);

        }

 

   Et voici le code de la fonction DrawMap() :

private void DrawMap()
{
Point cameraPoint = VectorToCell(camera.Position);
Point viewPoint = VectorToCell(camera.Position + ViewPortVector());
 
Point min = new Point();
Point max = new Point();
 
min.X = cameraPoint.X;
min.Y = cameraPoint.Y;
max.X = (int)Math.Min(viewPoint.X, map.GetLength(1));
max.Y = (int)Math.Min(viewPoint.Y, map.GetLength(0));
 
Rectangle tileRectangle = new Rectangle( 0, 0,
                                           tileWidth,
                                               tileHeight);
 
spriteBatch.Begin();
 
for (int y = min.Y; y < max.Y; y++)
{
for (int x = min.X; x < max.X; x++)
{
tileRectangle.X = x * tileWidth - (int)camera.Position.X;
tileRectangle.Y = y * tileHeight - (int)camera.Position.Y;
spriteBatch.Draw( tiles[map[y, x]],
                        tileRectangle,
                             Color.White);
}
}
 
spriteBatch.End();
}

 

    Nous voyons que la fonction Draw() ne fait qu'appeler la fonction DrawMap(). C'est dans celle-ci que se fait tout le rendu : cool

   Tout d'abord, nous voyons que la position de la caméra nous indique la tile dans laquelle elle se trouve et dans la seconde ligne nous voyons quelle devrait être la tile à la position de la caméra augmentée de la valeur du ViewportVector.

   La variable locale min de type Point est initialisée pour mémoriser la tile à partir de laquelle le rendu va se faire tandis que la variable max utilise la méthode Min de la classe Math pour retourner le minimum des deux valeurs passées à la méthode (nous savons que ViewPortVector a en mémoire la taille de l'ecran augmentée de la valeur d'une tile).

   Si nous essayons d'effectuer le rendu quand la caméra se trouve à sa position maxi augmentée de la valeur d'une tile nous allons provoquer une erreur tandis que si comme valeur maxi en X nous donnons la valeur (int)Math.Min(viewPoint.X, map.GetLength(1)) et à Y la valeur (int)Math.Min(viewPoint.Y, map.GetLength(0)), nous voyons que nous n'effectuerons pas de rendu en dehors des limites de notre map. wink

   Nous créons ensuite un objet tileRectangle de type Rectangle et qui va contenir la position des tiles à dessiner sur l'écran. En effet, dans les deux boucles suivantes, les valeurs y et x varient respectivement de min.Y à max.Y et de min.X à max.X.

   La valeur de tileRectangle.X est alors trouvée en multipliant x par la largeur d'une tile moins la valeur de la position en x de la caméra et la valeur de tileRectangle.Y est trouvée en multipliant y par la hauteur d'une tile moins la valeur de la position en y de la caméra.

   Pour terminer, la méthode Draw() de l'objet spriteBatch dessine toutes les tiles à l'emplacement défini par notre rectangle tileRectangle.

 

   Et c'est tout pour ce chapitre ! smiley Celui-ci nous a entraîné dans des concepts un peu plus compliqués que ceux vus précédemment mais je pense qu'il était intéressant de les mentionner ici. Vous pourrez dès lors réutiliser ces nouvelles fonctions dans vos projets de tilemapping pour apporter un peu plus d'efficacité lors du rendu de vos tiles. cool


   A bientôt pour le prochain tuto Xna chapitre 9 : La Caméra : 3ème partie – Les Effets de Zoom ! smiley


      Gondulzak.  

 

 

Connexion

CoalaWeb Traffic

Today97
Yesterday126
This week223
This month3897
Total1743104

23/04/24