Programmation graphique

Chapitre 7 : La Caméra - 1ère partie

 

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

 

      Préliminaires

    Ce chapitre va vous montrer combien il est facile de mettre en oeuvre une simple caméra dans une fenêtre de jeu. La mise en oeuvre de ce "scrolling" de caméra devient une nécessité lorsque la scène où se déroule l'action est plus large que l'écran, ce qui est le plus souvent le cas. cheeky

   Dans ce chapitre nous allons faire défiler une map dans quatre directions (haut, bas, gauche, droite). Nous allons utiliser pour ce faire, un tableau d'entiers dans lequel nous chargerons des sprites à partir d'une List<Texture2D>.  

 

 

   Les listes étant inconnues de l'utilisateur C, je vais commencer par quelques explications (ou un petit rappel wink) concernant les listes et les tableaux bi-dimensionnels en C#.

     Un peu de théorie...

             a – La classe List

   Supposons que nous désirions créer une liste d'objets quelconques dont nous ne connaissons pas a priori la quantité contenue dans cette liste, laquelle nous voudrions à loisir, agrandir ou raccourcir selon nos besoins. Dans ce cas précis, l'utilisation d'un tableau ne serait pas très judicieux. frown

   A cet effet, le C# nous permet d'utiliser une classe générique, la classe List. D'une manière générale, un type générique, classe ou structure, nous permet (de la même manière que les Templates du C++) de choisir le type que nous désirons donner à la classe que nous voulons créer.
   Mais pour l'instant, nous avons juste besoin de savoir que le C# nous permet de déclarer une classe List du type qui nous intéresse. Une déclaration se fait de la manière suivante :

List<int> listOfInts = new List<int>();       
//dans le cas où nous voudrions créer une liste d'entiers
ou
List<string>listOfNames = new List<string>();       
//dans le cas où nous voudrions créer une liste de strings.

 

   Retenons simplement que, par exemple, pour remplir une liste quelconque nous utiliserons la fonction Add() de la classe List. Ainsi, pour ajouter 3 noms dans une liste de strings nous écrirons :

listOfNames.Add(«Jay»);
listOfNames.Add(«Tankerpat»);
listOfNames.Add(«Gondulzak»);

 

...liste dans laquelle Jay sera l'élément 0, Tankerpat l'élément 1 et Gondulzak l'élément 2.

   Et nous irons dès lors chercher le nom d'indice X en utilisant la méthode ElementAt(X). Soit pour retrouver Tankerpat nous écrirons

string Name = listOfNames.ElementAt(1);

 

Si l'on veut supprimer le dernier élément de la liste nous écrirons simplement  :

listOfStrings.RemoveAt(2);

 

   Il y a une autre façon d'introduire ou de supprimer des éléments dans une liste, on peut également utiliser les braquets ( [ et ] ). On peut ainsi remplacer le dernier élément que nous venons de supprimer en écrivant :

listOfNames[2] = «MachinChose»;

 

   Nous dirons pour terminer qu'une liste peut-être complètement supprimée en écrivant :


listOfNames.Clear();

   et que pour connaitre le nombre d'éléments d'une liste nous écrirons :

int nbElementsDeLaListe = listOfNames.Count();

 

   Et pour notre premier exemple de défilement de map nous allons avoir besoin d'une liste de tiles, soit d'une liste de type Texture2D. Nous écrirons donc pour l'initialisation :

List<Texture2D> tiles = new List<Texture2D>();

 

             b – Un tableau bi-dimensionnel

   Je ne vais pas ici réécrire la théorie des tableaux, celle ci est assez répétée dans l'apprentissage du langage C. Il faut simplement savoir qu'en C#, l'initialisation d 'un tableau bi-dimensionnel d'entiers contenant par exemple 6 lignes et 10 colonnes peut s'écrire :

int[,] tableau = new int[6, 10];

   Dans notre premier exemple, notre map sera précisément construite avec un tableau bi-dimensionnel d'entiers, nous allons bientôt le voir. smiley

 

    


      Défilement d'une map dans 4 directions

   Après ce petit détour théorique, nous reprenons maintenant nos bonnes habitudes et nous créons un nouveau projet que nous allons par exemple nommer DefilMap. Et afin d'aérer quelque peu le code, désormais nous ne réécrirons plus toutes les remarques expliquant l'utilité de chaque fonction. Maintenant que nous avons déjà écrit plusieurs projets, nous n'en avons plus besoin. Et comme notre fichier n'est pas très long, je vais écrire le code en entier, les explications nécessaires à la bonne compréhension du projet viendront ensuite. wink

 

//DefilMap
//Game1.cs
 
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 DefilMap
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
KeyboardState currentState;
 
int cameraPositionX;
int cameraPositionY;
int cameraSpeed;
 
List<Texture2D> tiles = new List<Texture2D>();
 
int tileWidth;
int tileHeight;
 
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 Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
 
 
protected override void Initialize()
{
tileWidth = 32;
tileHeight = 32;
cameraSpeed = 6;
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: Unload any non ContentManager content here
}
 
 
protected override void Update(GameTime gameTime)
{
currentState = Keyboard.GetState();
 
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
 
if (currentState.IsKeyDown(Keys.Escape))
this.Exit();
 
if (currentState.IsKeyDown(Keys.Up))
ScrollUp();
 
else if (currentState.IsKeyDown(Keys.Left))
ScrollLeft();
 
else if (currentState.IsKeyDown(Keys.Down))
ScrollDown();
 
else if (currentState.IsKeyDown(Keys.Right))
ScrollRight();
 
LockCamera();
base.Update(gameTime);
}
 
private void ScrollUp()
{
cameraPositionY -= cameraSpeed;
}
 
private void ScrollRight()
{
cameraPositionX += cameraSpeed;
}
 
private void ScrollDown()
{
cameraPositionY += cameraSpeed;
}
 
private void ScrollLeft()
{
cameraPositionX -= cameraSpeed;
}
 
private void LockCamera()
{
cameraPositionX = (int)MathHelper.Clamp(
cameraPositionX, 0,
tileWidth * map.GetLength(1) - GraphicsDevice.Viewport.Width);
cameraPositionY = (int)MathHelper.Clamp(
cameraPositionY, 0,
tileHeight * map.GetLength(0) - GraphicsDevice.Viewport.Height);
}
 
protected override void Draw(GameTime gameTime)
{
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 - cameraPositionX,
y * tileHeight - cameraPositionY,
tileWidth,
tileHeight),
Color.White);
}
}
 
base.Draw(gameTime);
 
spriteBatch.End();
}
}
}

   Voyons ce qui se passe dans ce code, quelques-un de ses élément doivent vous être familiers, maintenant. wink

        Déclarations

   Ok, notre projet est maintenant écrit, nous allons donc décortiquer tout ce qu'il faut en savoir.

   A l'intérieur de la classe Game1(), nous déclarons nos variables et créons notre map. Pour déplacer cette map dans les quatre directions, nous avons besoin d'une variable qui va détecter quelle touche est frappée sur le clavier. La classe KeyboardState sera donc utilisée comme type pour la variable currentState qui mémorisera l'état actuel de la touche pressée.

 

KeyboardState currentState;

 

   Nous déclarons ensuite les variables cameraPositionX, cameraPositionY et cameraSpeed qui représenteront respectivement la position de notre caméra en x et y ainsi que la vitesse de celle-ci.


   Vient ensuite la déclaration de notre liste de type Texture2D dont nous avons parlé plus haut et dans laquelle nous allons introduire notre série de tiles. Il s'agit de 10 tiles de type png (à télécharger avec le projet, ci-dessus) que nous allons charger ultérieurement à l'aide de la méthode LoadContent().

   Pour terminer, nous déclarons deux entiers qui représenteront la largeur et la hauteur de nos tiles et nous construisons notre tableau bi-dimensionnel qui va représenter notre fenêtre.

 

Il faut un certain temps pour construire un tel niveau à la main. Je vous le donne ici tout fait dans un but pédagogique mais si plus tard vous désirez créer un grand nombre de niveaux de dimensions raisonnables (et donc bien plus grandes), vous devrez utiliser un level Editor pour plus de simplicité (je verrai plus tard la possibilité de créer un level-editor avec les Windows forms en C# cool).

 

   Maintenant il est nécessaire de savoir que les chiffres ont une importance capitale dans cette configuration et nous allons en reparler dans la fonction LoadContent().

        Fonction Initialize()

   La fonction Initialize() donne des valeurs aux variables tileWidth, tileHeight, cameraPositionX, cameraPositionY et cameraSpeed. Nous plaçons donc ici notre caméra aux coordonnées (0,0) mais rien ne vous empêche de la placer ailleurs, par exemple au milieu de la fenêtre. wink

 

       Fonction LoadContent()

   Et comme à son habitude, la fonction LoadContent() va charger nos tiles. Je reviens sur l'importance des chiffres qui sont adjoints à ces tiles. Nous ne devons pas ignorer que la première tile chargée dans la méthode LoadContent() possède l'indice 0, la seconde l'indice 1, etc... 


   Nous voyons donc que lors de la création de notre map, nous devrons mettre à l'endroit désiré, le bon indice de la tile de la liste chargée dans LoadContent() (Nous l'avons vu dans notre rappel théorique, à l'aide de la méthode Add() de la classe List wink).


        Fonction Update()

   L'appel aux fonctions de défilement de la fenêtre va se faire selon le choix de la touche de direction pressée.

   Les fonctions ScrollUp(), ScrollLeft(), ScrollDown() et ScrollRight() seront en effet respectivement appelées lors d'un appui sur les touches HAUT, GAUCHE, BAS et DROITE. Toutes ces méthodes sont sensiblement les mêmes, elles changent les valeurs de cameraPositionX et cameraPositionY et ce, en fonction de la touche pressée.

 

       Fonctions de déplacement de la camera

   Nous voyons par exemple que pour déplacer la caméra vers le haut nous devons soustraire de cameraPositionY (soit tendre vers 0 sur l'axe Y) une valeur égale à la vitesse donnée à la caméra. Et pour déplacer la caméra vers le bas nous devons ajouter à cameraPositionY (soit tendre vers le bas de la fenetre) une valeur égale à la vitesse donnée à la caméra.

   De même, pour les mouvements horizontaux de la caméra, nous voyons que nous devons soit ajouter la valeur de la vitesse de la camera à la variable cameraPositionX pour un déplacement vers la doite, soit soustraire la valeur de cette même vitesse à cameraPositionX pour un déplacement vers la gauche.

   Nous donnerons en dernier les explications nécessaires à l'utilité de la fonction private void LockCamera() et nous passons de suite à notre fonction de dessin. cool

 

       Fonction Draw()

   Regardons de près notre fonction Draw(). Celle-ci dessine notre map à l'écran (le tableau bi-dimensionnel d'entiers que nous avons crée à la suite de nos déclarations) à l'aide de deux boucles for qui parcourent l'une chaque ligne et l'autre, chaque colonne du tableau.

   Nous savons quelle tile dessiner en utilisant la valeur de map[y,x] comme étant l'index de la tile dans la liste de tiles et nous savons que pour trouver l'endroit où doit être dessinée cette tile nous multiplions la valeur x par la largeur de cette tile pour obtenir son abcisse et en multipliant la valeur y par la hauteur de la tile pour obtenir son ordonnée.

   Et nous dessinons le tout en utilisant une fonction Draw() surchargée de la classe SpriteBatch qui utilise un rectangle de destination.

   Nous voyons en outre que les deux boucles utilisent les fonctions GetLength(0) et GetLength(1). Ces deux fonctions retournent respectivement les valeurs du nombre de lignes et du nombre de colonnes d'un tableau bi-dimentionnel.

 

        Fonction LockCamera

   Cette fonction a pour effet de bloquer la camera dans les limites de la fenêtre. Je vais en premier lieu vous montrer ce que vous risqueriez d'obtenir si cette fonction n'était pas utilisée (cf ci-dessous). cheeky Vous serez d'accord avec moi pour dire que ce n'est pas le résultat que vous escomptiez obtenir ! laugh

 

Image du projet compilé sans la fonction LockCamera().

 

   Bien, voyons maintenant le code la fonction LockCamera() :


   On utilise la méthode Clamp() de la classe MathHelper pour bloquer la caméra. La valeur minimum de cameraPositionX est bien entendu 0. Pour empêcher la caméra d'aller au-delà du côté droit de la carte, il faut en premier lieu connaitre la largeur de la map en pixels. Or nous connaissons la largeur d'une tile en pixels et nous savons également depuis les explications données de la fonction Draw() que le nombre de tiles d'une ligne est donnée par la méthode GetLength(0). Nous devons donc multiplier la longueur d'une tile par le nombre de tiles d'une ligne, valeur de laquelle nous retirons la longueur de la fenêtre en pixels donnée par GraphicsDevice.Viewport.Width.

 

  Et nous ferons la même chose pour bloquer la caméra vers le haut et vers le bas en passant les valeurs minimum et maximum qui représentent la hauteur de notre fenêtre. La valeur minimum de cameraPositionY est également 0 et nous savons que le nombre de tiles d'une colonne est donnée par la méthode GetLength(1). Nous devrons multiplier la hauteur d'une tile par le nombre de tiles d'une colonne, valeur de laquelle nous allons retirer la hauteur de la fenêtre en pixels donnée par GraphicsDevice.Viewport.Height.

 

   Et pour terminer, nous ferons appel à la fonction LockCamera() en plaçant celle-ci à la fin de la fonction Update(), juste avant l'instruction base.Update(gameTime);


   Voilà, c'est tout pour ce premier exemple. cool

   Sauvegardez votre 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. C'est le résultat que nous voulions obtenir. smiley

 

Vous pouvez maintenant déplacer votre tableau de tiles sans sortir des limites de votre fenêtre.



   A bientôt pour le prochain tuto Xna chapitre 8 (La Caméra – 2ème partie) ! smiley

        Gondulzak.

 

 

 
 
 

Connexion

CoalaWeb Traffic

Today116
Yesterday232
This week1142
This month3435
Total1742642

20/04/24