Programmation graphique

Chapitre 9 : La Caméra - 3ème partie : le zoom

 

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

 

      Préliminaires

   Nos deux chapitres précédents concernant la caméra nous ont permis de faire défiler une map dans les quatre directions à l'aide des touches fléchées. Ils vous ont également expliqué une manière plus efficace d'effectuer un rendu à l'écran.

   Dans ce chapitre nous allons maintenant présenter une façon relativement simple d'effectuer un zoom avant et arrière du contenu de l'écran à l'aide de la caméra. smiley

   On se lance dans le code sans plus tarder ! cool Comme d'habitude nous ouvrons notre IDE et nous créons un nouveau projet que nous allons par exemple nommer «SimpleCameraZoom».

   Effacez tout le contenu du fichier Game1.cs et entrez le code suivant à la place. Les explications nécessaires à sa bonne compréhension seront données par la suite. 

 

 

        Projet : SimpleCameraZoom                                            

              1 – Le code

 
//Simple Camera Zoom
//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 SimpleCameraZoom
{
 
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
 
//Données sources
private Texture2D background;
private Texture2D planete;
private Texture2D AstronefCycleTexture;
private Rectangle currentFrameLocation;
private Vector2 astronefFrameOrigin;
 
//Données de destination
private Vector2 astronefPosition;
private Vector2 planetPosition;
private Vector2 cameraPosition;
private Vector2 cameraOffset;
 
//Données d'animation
private int currentFrame;
private int numberOfFrames;
 
private int millisecondsUntilNextFrame;
private int millisecondsPerFrame;
 
private SpriteEffects spriteEffet;
 
private float zoomLevel;
 
 
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
 
 
protected override void Initialize()
{
graphics.PreferredBackBufferHeight = 480;
graphics.PreferredBackBufferWidth = 800;
 
//graphics.IsFullScreen = true;
graphics.ApplyChanges();
 
numberOfFrames = 4;
currentFrame = 0;
 
millisecondsPerFrame = 15;
millisecondsUntilNextFrame = millisecondsPerFrame;
 
currentFrameLocation = new Rectangle(0, 0, 80, 55);
astronefFrameOrigin = new Vector2(80, 55);
 
astronefPosition = new Vector2(200, 200);
cameraOffset = new Vector2(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight/2);
 
cameraPosition = astronefPosition;
planetPosition = Vector2.Zero;
 
spriteEffet = SpriteEffects.None;
 
zoomLevel = 1.0f;
 
base.Initialize();
}
 
 
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
background = Content.Load<Texture2D>("PrimaryBackground");
AstronefCycleTexture = Content.Load<Texture2D>("fusee");
planete = Content.Load<Texture2D>("Earth");
}
 
 
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
 
 
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
      this.Exit();
 
millisecondsUntilNextFrame -= gameTime.ElapsedGameTime.Milliseconds;
 
if (Keyboard.GetState().IsKeyDown(Keys.Up))
{
     zoomLevel += 0.01f;
}
 
else if (Keyboard.GetState().IsKeyDown(Keys.Down))
{
if (zoomLevel >= 0.68) //Afin de ne pas sortir du cadre du background
      zoomLevel -= 0.01f;
}
 
if (Keyboard.GetState().IsKeyDown(Keys.Left))
{
      astronefPosition.X -= 10;
      spriteEffet = SpriteEffects.None;
}
else if (Keyboard.GetState().IsKeyDown(Keys.Right))
{
astronefPosition.X += 10;
spriteEffet = SpriteEffects.FlipHorizontally;
}
 
if (millisecondsUntilNextFrame <= 0)
{
currentFrame++;
millisecondsUntilNextFrame = millisecondsPerFrame;
}
 
if (currentFrame >= numberOfFrames)
currentFrame = 0;
 
currentFrameLocation.X = currentFrameLocation.Width * currentFrame;
 
float coefMult = 0.05f;
 
if (cameraPosition.X < astronefPosition.X)
{
cameraPosition.X -= ((cameraPosition.X - astronefPosition.X) * coefMult);
}
else if (cameraPosition.X > astronefPosition.X)
{
cameraPosition.X += ((cameraPosition.X - astronefPosition.X) * -coefMult);
}
 
base.Update(gameTime);
}
 
 
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
 
Vector2 drawLocation = cameraPosition - (cameraOffset / zoomLevel * 3/2);
Matrix scaleMatrix = Matrix.CreateScale(zoomLevel);
 
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.AnisotropicClamp,
DepthStencilState.Default,
RasterizerState.CullNone,
null,
scaleMatrix);
 
spriteBatch.Draw(background, Vector2.Zero, Color.White);
spriteBatch.Draw(planete, planetPosition, Color.White);
 
 
spriteBatch.Draw(AstronefCycleTexture,
astronefPosition - drawLocation,
currentFrameLocation,
Color.White,
0.0f, //Rotation
astronefFrameOrigin, //Origine
1.0f, //Echelle
spriteEffet,
0.0f);
 
spriteBatch.End();
 
base.Draw(gameTime);
}
}
}

 

 

       2 – Analyse

   Données sources :

   Les données sources : outre l'image de notre background étoilé et celle de la planète Terre, la variable Texture2D AstronefCycleTexture représente l'image du cycle des 4 frames de notre astronef.

    currentFrameLocation, de type  Rectangle, représente quant à elle, l'emplacement de la frame courante et astronefFrameOrigin,  de type Vector2, représente les dimensions de la frame d'origine de cette feuille de sprites.

 

  Données de destination :

   Les données de destination, de type Vector2,  nous renseigneront sur les positions de l'astronef, de la planète, ainsi que sur la position de la caméra après chaque mouvement de zoom avant ou de zoom arrière.

 

   Données d'animation :

   Nous avons déjà vu ce que représentaient les données d'animation lors d'un précédent chapitre mais je vais néanmoins en faire un rappel :
   - currentFrame : position de la frame courante par rapport aux 4 frames de la feuille de sprites, numérotées 0, 1, 2 et 3.
   - numberOfFrames : nombre de frames de notre feuille de sprites (ici, 4).
   - millisecondsUntilNextFrame : le nombre de millisecondes restantes avant l'affichage de la frame suivante.
   - millisecondsPerFrame : temps d'affichage d'une frame à l'écran.
   - spriteEffet : effet donné à la direction du sprite selon que l'on presse la touche flèche gauche ou flèche droite.
   - zoomLevel : cette variable est nouvelle pour notre projet, il s'agit du niveau de zoom au départ.

      

   Fonction Initialize()                                                                                                                                      

   C'est ici que nous initialisons toutes nos variables. Pour commencer, nous nous permettons la possibilité de choisir le plein écran si nous le désirons. cool

   Le nombre de frames (nous l'avons vu dans la feuille de sprites) a ici la valeur 4 et la frame courante est bien entendu initialisée à 0 (1ère frame).

   On donne la valeur 15 au nombre de millisecondes par frames. Il s'agit d'une valeur relativement peu élevée mais vous pouvez changer cette valeur afin de mieux voir le comportement de l'astronef  dans un zoom rapproché.

   Nous donnons ensuite cette valeur à la variable représentant le nombre de millisecondes restantes jusqu'à l'affichage de la frame suivante car ces valeurs sont égales au début de l'affichage de chaque frame.

   CurrentFrameLocation représente les données de la frame courante de la feuille de sprites soit : x = 0, y = 0, longueur de frame = 80 et hauteur 55. 

   Les valeurs données à astronefFrameOrigin (80, 55) sont choisies de façon telle à avoir un déplacement réaliste de l'astronef vers la planète lors d'un zoom avant. Vous pouvez changer ces valeurs et voir le comportement de l'astronef par rapport à la Terre lors d'un même zoom avant. wink

   Nous centrons la caméra au milieu de l'écran (cameraOffset), mais comme celle-ci va devoir suivre le déplacement de l'astronef, nous lui donnerons comme coordonnées, celles de l'astronef à sa position d'origine et nous verrons dans la fonction Update() de quelle manière variera la  position de la caméra en  fonction du déplacement de l'astronef.

   Nous plaçons ensuite l'image de la planète en position (0, 0)  et nous initialisons la variable spriteEffet à  SpriteEffects.None (le sens d'orientation de l'astronef étant vers la gauche au départ du programme). 

   Et pour terminer, nous donnons la valeur 1.0f comme niveau de zoom à l'origine. cool

  

   Fonction LoadContent()

   N'oubliez pas de charger vos assets dans la fonction LoadContent() en faisant un clic droit sur SimpleCameraZoomContent(Content) dans l'explorateur de solutions afin d'ajouter les trois images dont nous avons besoin pour notre projet. cheeky

  Bien, c'est fait ? wink  Nous allons maintenant passer à la partie la plus importante du projet. devil

 

   Fonction Update()

   Premièrement, nous soustrayons le temps écoulé à la variable millisecondsUntilNextFrame afin de permettre au système graphique d'afficher la frame suivante.

   Nous pouvons maintenant nous pencher sur notre série de tests que contient cette fonction :
   - Testons un appui sur les quatre touches fléchées. Nous avons vu un peu plus haut que nous avons donné 1.0f comme niveau de zoom à l'origine.
   - La pression de la touche HAUT va augmenter notre niveau de zoom (nous utilisons exprès une faible valeur « 0.01f » afin que la vitesse du zoom nous donne une bonne visibilité du déplacement.
   - La touche BAS, quant à elle (vous l'auriez deviné... cheeky) produit l'effet inverse, c'est-à-dire un zoom arrière en retirant la valeur 0.01f à la variable zoomLevel.
     Et dans ce cas de pression sur la touche BAS vous voyez que je fais un test sur la valeur de zoomLevel :
 
                       if (zoomLevel  >= 0.675)    //Afin de ne pas sortir du cadre du background
                                 zoomLevel -= 0.01f;

 

   En effet, comme je l'indique en remarque dans le code, si on continue à appuyer sur la touche BAS, l'effet d'éloignement produit par le zoom arrière va nous faire atteindre et dépasser les limites de notre background et nous allons voir notre astronef sortir du cadre de son propre espace (Voir figure suivante). laugh

                                          

Image obtenue en pressant la touche BAS sans test sur la variable zoomLevel.

  

   Mais alors, d'où vient cette valeur 0.675f ? frown Eh bien, celle-ci a été obtenue par essais et erreurs en vérifiant tout simplement à chaque test que je ne dépassais pas le cadre du background (eh oui, on s'amuse bien ! wink). Je rappelle que cette valeur est fonction de la hauteur de notre background et que celui-ci a une dimension de 720 pixels de haut.

     Et le test sur la variable zoomLevel, bloquée à la valeur 0.675f nous donne donc l'éloignement maximum suivant :

 

 

   Nous verrons les images obtenues par zoom avant, en fin de chapitre mais pendant qu' Aron explore l'Espace, nous, on continue l'exploration de notre fonction Update(). cool

 

   - Une pression sur les touches GAUCHE et DROITE déplace l'astronef dans le sens des flèches mais ici aussi nous allons veiller à limiter son déplacement  afin qu'il ne puisse pas sortir de l'écran par la droite, comme nous allons le voir bientôt. wink

   Nous faisons ensuite un test sur la variable millisecondsUntilNextFrame. Si le temps restant est inférieur ou égal à 0, on passe à la frame suivante.

   Et dans le test suivant nous réinitialisons currentFrame à 0 dès qu'un cycle d'affichage est bouclé. Nous n'oublions pas en outre d'indiquer où chercher l'abscisse de la frame à afficher en donnant à currentFrameLocation  la valeur du produit de sa position dans la feuille de sprites (0, 1, 2, 3) par la longueur de la frame (80).

   Nous allons maintenant procéder au déplacement de notre caméra. smiley Les deux tests suivants comparent la position de la caméra par rapport à l'astronef.

   Si la position de la caméra en x est inférieure à celle de l'astronef en x, nous la modifions en lui soustrayant le produit des différences de leurs positions par un coefficient multiplicateur positif.

   Si la position de la caméra en x est supérieure à celle de l'astronef en x, nous la modifions en lui ajoutant le produit des différences de leurs positions par un coefficient multiplicateur négatif. Vous voyez que ce coefficient multiplicateur coefMult  possède une valeur de 0.05f mais… indecision

  

     …que représente-t-il ?

   Je vous ai dit un peu plus haut que nous devions limiter le déplacement de l'astronef par la pression sur les touches GAUCHE et DROITE afin d'en garder la visibilité à l'intérieur de notre écran. Voilà donc à quoi sert ce coefficient multiplicateur. Pour mieux vous en rendre compte, je vous conseille de faire des essais en lui donnant des valeurs différentes. Vous voyez que plus la valeur est basse et plus l'astronef peut voyager vers la gauche ou la droite et même sortir de l'écran ! laugh

   Vous vous apercevrez aussi qu'une valeur d'une unité (1.0f), donnée à la variable coefMult empêche tout déplacement de l'astronef (à gauche ou à droite) et ne permet simplement que de se retourner vers la  gauche ou vers la droite grâce à l'état de la variable spriteEffet.

   Voilà pour la fonction Update(), nous pouvons maintenant passer à la fonction Draw(). 

 

   Fonction Draw()

   Pour commencer, nous initialisons une nouvelle variable au début de la fonction. Ici, la variable drawLocation de type Vector2, positionne l'emplacement de l'astronef au départ du programme par rapport au quotient du positionnement de la caméra par son niveau de zoom. J'utilise les valeurs de l'expression « ((cameraOffset / zoomLevel * 3) /2) » de façon arbitraire afin de positionner l'astronef à un endroit qui me semblait suffisamment éloigné de la Terre. wink

   Je laisse ces valeurs à votre appréciation mais vous pouvez les changer à votre convenance. Vous verrez par exemple que l'expression ((cameraOffset / zoomLevel * 2) /2) positionne l'astronef dans le coin inférieur droit de l'écran mais vous pouvez le placer où vous voulez. cool

   Ensuite, afin que la fonctionnalité du zoom de la caméra puisse s'appliquer à tout l'écran, nous devons créer une matrice pour en modifier l'échelle, matrice que nous allons utiliser dans la fonction Begin() de la classe SpriteBatch. Ceci est nouveau car jusqu'ici nous n'avions pas encore utilisé de fonction Begin() surchargée et ici nous allons devoir le faire ( la théorie sur les matrices sort du cadre de ce tutoriel et je vous renvoie à vos cours de maths ou aux nombreux tutoriels que vous rencontrerez sur le net à ce sujet. Tout ce que nous avons besoin de savoir actuellement est que la matrice créée va charger toutes les données nécessaires concernant les informations sur comment se font les déplacements et sur les différentes échelles de zoom).

 

   Pour en revenir à notre fonction Begin(), il s'agit d'une des surcharges de la fonction Begin() données par la librairie MSDN dont je vous donne une citation ici :

 

   Citation de la libraire MSDN
     //Démarre une opération par lot de sprite à l'aide des objets d'état de tri, de fusion,

     //d'échantillonnage, de stencil de profondeur et de rastérisation définis, plus un effet
     //personnalisé et une matrice de transformation 2D. Si l'un de ces objets d'état a la valeur  nulle,
     //les objets d'état par défaut sont sélectionnés (BlendState.AlphaBlend, DepthStencilState.None,
     //RasterizerState.CullCounterClockwise, SamplerState.LinearClamp). En cas d'effet nul, le nuanceur
     //SpriteBatch Classe par défaut est sélectionné.

 

   Voilà en ce qui concerne l'explication de cette surcharge de la fonction Begin() et pour terminer l'écriture de notre fonction Draw(), nous n'avons plus qu'à dessiner notre background, la planète Terre ainsi que l'astronef.

   Je pense avoir donné les explications nécessaires détaillées pour permettre la compréhension d'un simple effet de zoom et puisque nous avons déjà vu une image de zoom arrière, je vous laisse regarder maintenant trois captures d'écran de zoom avant prises à des moments différents. yes

 

 

 

 

   Voilà, c'est tout pour ce tutoriel, j'espère qu'il vous permettra d'obtenir de jolis effets dans vos projets de jeux. cool

   J'ai récemment signalé sur le site, dans le forum « Remarques sur les tutoriels », qu'un nouveau Big Tuto Xna, ou plutôt une suite à celui-ci allait être prochainement mis en oeuvre. Il s'agit d'un shoot spatial dans lequel le héros sera notre ami Aron que vous voyez ci-dessus et qui va se déplacer et combattre des aliens dans un espace en perpétuel mouvement.

   Je travaille sur ce projet depuis quelque temps déjà et celui-ci est actuellement réalisé à hauteur de 60% environ. Des améliorations sont continuellement apportées sur le programme mais d'autres restent à faire et notamment au niveau du design. Une quinzaine de tutoriels seront nécessaires à la présentation de ce projet sur le site. smiley

   Malgré tout le travail auquel il doit déjà faire face, notre ami Jay a accepté de m'aider à la réalisation de ce projet qui me tient à coeur et je l'en remercie profondément. Restez donc tous bien attentifs aux discussions du forum sur ce sujet et je vous dis à bientôt pour une nouvelle expérience Xna.

 

          Gondulzak. smiley

 

 

Connexion

CoalaWeb Traffic

Today232
Yesterday282
This week1026
This month3319
Total1742526

19/04/24