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.
On se lance dans le code sans plus tarder ! 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 :
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.
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.
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.
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.
Bien, c'est fait ? Nous allons maintenant passer à la partie la plus importante du projet.
Fonction Update()
Premièrement, nous soustrayons le temps écoulé à la variable millisecondsUntilNextFrame afin de permettre au système graphique d'afficher la frame suivante.
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).
Image obtenue en pressant la touche BAS sans test sur la variable zoomLevel.
Mais alors, d'où vient cette valeur 0.675f ? 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 ! ). 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().
- 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.
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. 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…
…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 !
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.
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.
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.
Voilà, c'est tout pour ce tutoriel, j'espère qu'il vous permettra d'obtenir de jolis effets dans vos projets de jeux.
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.
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.