Le défilement parallaxe
Le défilement parallaxe est souvent utilisé pour 'simuler' l'effet de profondeur dans un environnement en 2D (Mario, dessins animés Disney...). Cette technique également souvent utilisée pour créer des cinématiques, même dans des jeux en 3D.
L'objectif de cet article est d'implémenter le défilement parallaxe sur XNA 4.0.
Le code source est disponible ici.
Attention, cet article est destiné aux développeurs maîtrisant déjà le langage C# et ayant des bases dans le développement XNA.
L'objectif de cet article est d'implémenter le défilement parallaxe sur XNA 4.0.
Le code source est disponible ici.
Attention, cet article est destiné aux développeurs maîtrisant déjà le langage C# et ayant des bases dans le développement XNA.
La théorie
Le défilement parallaxe est une technique permettant de simuler une perspective de mouvement. Cette technique fonctionne grâce à la superposition de plusieurs couches évoluant à différentes vitesses. Le menu développé précédemment est parfait pour ce tutoriel. On va pouvoir le réutiliser et l'adapter pour s'en servir de système gérant le défilement parallaxe.
Le défilement
Ce défilement souvent infini se fait en fonction du joueur. Habituellement, le joueur avance dans ce décor, ici, le joueur reste à sa place (ou bien ne bouge que très peu) et la scène recule devant l'avancée du joueur. Comment arriver à avoir un défilement infini? Une très grande texture pourrait faire l'affaire mais cela est bien trop couteux (la texture prendrait trop de place) et on se rend compte qu'aussi grande soit la texture, elle n'est potentiellement pas infinie.
Pour répondre à cette problématique, on va pouvoir forcer la carte graphique à répéter indéfiniment une texture sur une plus grande surface. Mais une plus grande surface n'en fait toujours pas une surface potentiellement infinie, en effet! Pour arriver à un tel résultat, il va falloir tromper l'œil du joueur. En affichant une texture qui se répète sans jointure apparente, on est capable de déplacer la texture et la replacer 'au bon endroit'. Le joueur n'y verra que du feu, pour lui, la texture avance sans cesse, bien que le motif se répète, alors qu'en réalité, elle avance, et lorsque qu'elle atteint un certain point, elle 'recule' pour coïncider avec une des jointures de la répétition.
Pour répondre à cette problématique, on va pouvoir forcer la carte graphique à répéter indéfiniment une texture sur une plus grande surface. Mais une plus grande surface n'en fait toujours pas une surface potentiellement infinie, en effet! Pour arriver à un tel résultat, il va falloir tromper l'œil du joueur. En affichant une texture qui se répète sans jointure apparente, on est capable de déplacer la texture et la replacer 'au bon endroit'. Le joueur n'y verra que du feu, pour lui, la texture avance sans cesse, bien que le motif se répète, alors qu'en réalité, elle avance, et lorsque qu'elle atteint un certain point, elle 'recule' pour coïncider avec une des jointures de la répétition.
L'implémentation en XNA 4.0 avec les explications détaillées
On va repartir du système de menu précédemment réalisé (code disponible ici), le modifier un peu pour l'implémentation du défilement parallaxe.
Adaptation du système de menus
On sous-entend maintenant que vous avez téléchargé le sample de menu, afin de partir sur une base commune.
Rappel : le système de menu se décompose en 3 parties :
Rappel : le système de menu se décompose en 3 parties :
- Les scènes abstraites, les briques de bases permettant de créer les différents menus et écrans de jeu grâce à un minimum de souplesse
- Les scènes concrètes, les différents écrans du système, empilables
- Le gestionnaire de scènes, la classe centrale du système
Nouvelle classe : le ParallaxScrollingScene
Créons tout de suite une nouvelle classe : le ParallaxeScrollingScene. Placez-la dans le répertoire Scenes/Core. La visibilité de cette classe est public, et implémentera la classe abstraite AbstractGameScene.
Voici les informations essentielles à stocker pour cette classe qui représentera une couche du système de défilement parallaxe :
Voici les informations essentielles à stocker pour cette classe qui représentera une couche du système de défilement parallaxe :
- un flottant compris entre 0 et 1, un coefficient permettant d'appliquer une vitesse de déplacement différent pour chaque couche. Plus la valeur est petite, plus le déplacement sera lent. À zéro (0), la scène sera immobile
- un ContentManager afin de charger l'image de la couche dans une Texture2D
- une chaine de caractères contenant le nom de l'image à charger
- un booléen permettant de savoir si la classe a bien été initialisée
- enfin un Vector2 permettant de sauvegarder la position courante de la couche
private float _depth; private ContentManager _content; private Texture2D _parallaxeTexture; private readonly string _textureName; private bool _initialized; private Vector2 _pos; /// /// Récupère la profondeur de la couche /// public float Depth { get { return _depth; } set { _depth = value; } } /// /// Récupère la largeur de la texture répétée /// C'est le nombre de fois que la texture peut être répétée dans /// l'écran en largeur plus 2, pour avoir de la marge dans le /// mouvement. /// private int Width { get { return ((SceneManager.Game.Window.ClientBounds.Width / _parallaxeTexture.Width) + 2) * _parallaxeTexture.Width; } } /// /// Récupère la hauteur de la texture répétée /// C'est le nombre de fois que la texture peut être répétée dans /// l'écran en hauteur plus 2, pour avoir de la marge dans le /// mouvement. /// private int Height { get { return ((SceneManager.Game.Window.ClientBounds.Height / _parallaxeTexture.Height) + 2) * _parallaxeTexture.Height; } } /// /// Récupère la position du milieu de l'écran /// private Vector2 Middle { get { float x = Modulate((SceneManager.Game.Window.ClientBounds.Width - _parallaxeTexture.Width) / 2f, _parallaxeTexture.Width); float y = Modulate((SceneManager.Game.Window.ClientBounds.Height - _parallaxeTexture.Height) / 2f, _parallaxeTexture.Height); return new Vector2(x, y); } } public Vector2 Position { get { return _pos; } set { _pos = value; } } |
La première méthode à implémenter sera le constructeur: nous y initialiserons quelques paramètres de la scène, sauvegarderons le nom de l'image à charger dans la texture et initialisons la position de la couche:
public ParallaxeScrollingScene(SceneManager sceneMgr, string textureName) : base(sceneMgr) { TransitionOnTime = TimeSpan.FromSeconds(0.5); TransitionOffTime = TimeSpan.FromSeconds(0.5); _textureName = textureName; Position = Vector2.Zero; } |
Ensuite, ce sera les méthodes LoadContent et UnloadContent, la première permet de charger l'image de la couche, la dernière permet de déchargr le contenu chargé en mémoire. On pensera à bien passer le booléen _initialized à true une fois l'initialisation terminée.
protected override void LoadContent() { if (_content == null) _content = new ContentManager(SceneManager.Game.Services, "Content"); _parallaxeTexture = _content.Load(_textureName); _initialized = true; base.LoadContent(); } protected override void UnloadContent() { _content.Unload(); } |
On continue, avec les méthodes Update et Draw, à la base d'une scène de jeu. Dans la méthode Update, on force le booléen coveredByOtherscene à faux car cette scène/couche est destinée à être recouverte. Dans la méthode Draw, on y dessine la texture en spécifiant bien que la texture doit être répétée, grâce au SamplerState.LinearWrap dans la méthode Begin du spriteBatch :
public override void Update(GameTime gameTime, bool othersceneHasFocus, bool coveredByOtherscene) { // Cette scène est destinée à être recouverte // coveredByOtherscene est donc forcée à false base.Update(gameTime, othersceneHasFocus, false); } public override void Draw(GameTime gameTime) { SpriteBatch spriteBatch = SceneManager.SpriteBatch; spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearWrap, DepthStencilState.Default, RasterizerState.CullNone); spriteBatch.Draw(_parallaxeTexture, _pos, new Rectangle(0, 0, Width, Height), Color.White, 0, Vector2.Zero, 1f, SpriteEffects.None, 1.0f); spriteBatch.End(); } |
Enfin, la partie mathématique, qui permet de repositionner correctement la couche en fonction du 'wrapping' :
/// /// Permet de replacer la valeur entre -modulo et 0 /// /// Valeur à replacer /// Borne inférieure /// private static float Modulate(float value, float modulo) { return ((value - modulo) % modulo); } /// /// Déplace la couche dans la direction donnée et replace la texture au besoin /// /// Direction du mouvement /// public void MoveBy(Vector2 direction) { _pos += direction * _depth; // Attends que la classe soit initialisée sinon le getter sur Middle ne fonctionne pas, car la texture // n'est pas encore chargée. if (_initialized) { // On bloque le mouvement en hauteur _pos.Y = MathHelper.Clamp(_pos.Y, Middle.Y + (Middle.Y*_depth), Middle.Y - (Middle.Y*_depth)); } // On repositionne les textures _pos.X = (_pos.X - _parallaxeTexture.Width) % _parallaxeTexture.Width; _pos.Y = (_pos.Y - _parallaxeTexture.Height) % _parallaxeTexture.Height; } |
Nouvelle classe : le ParallaxScrollerScene
Placez cette classe dans le répertoire Scenes/Core. Sa visibilité doit être public, et la classe implémentera la classe abstraite AbstractGameScene.
Voici les informations essentielles à stocker pour cette classe qui gèrera les différentes couches du déplacement parallaxe :
Voici les informations essentielles à stocker pour cette classe qui gèrera les différentes couches du déplacement parallaxe :
- une liste permettant de stocker les différentes couches
- une constante définissant le nombre maximum de couches que nous forcerons à 8, car il existe peu de cas où plus de 8 couches seront nécessaires
private readonly List<ParallaxeScrollingScene> _parallaxes; protected const int MaxLayer = 8; protected List<ParallaxeScrollingScene> Parallaxes { get { return _parallaxes; } } |
Le constructeur est simple: on y appelle le constructeur de base et on y initialise la liste :
public ParallaxeScrollerScene(SceneManager sceneMgr) : base(sceneMgr) { _parallaxes = new List<ParallaxeScrollingScene>(); } |
Voici une méthode exclusive à cette classe : AddLayer, qui permettra d'ajouter à la liste interne de scène, les différentes couches du défilement parallaxe :
public void AddLayer(ParallaxeScrollingScene scene) { scene.Depth = (float)_parallaxes.Count / MaxLayer; _parallaxes.Add(scene); } |
Les version surchargées de LoadContent et de Remove permettent de prendre en compte les différentes couches dans leur traitement. LoadContent ajoute à la liste des composants du jeu les différentes couches et Remove bien évidemment les retire:
protected override void LoadContent() { foreach (var parallaxeScrollingScene in Parallaxes) parallaxeScrollingScene.Add(); base.LoadContent(); } public override void Remove() { foreach (var scene in _parallaxes) scene.Remove(); base.Remove(); } |
L'intégration
Une fois le ParallaxeScrollingScene et le ParallaxeScrollerScene implémenter, il faut les intégrer !
Commençons par la modification de la classe GamePlayScene. Elle doit désormais étendre ParallaxeScrollerScene à la place d'AbstractGameScene. On va ensuite rajouter un appel de base sur les méthodes LoadContent, UnloadContent, HandleInput et Draw (cela se fait en appelant base.nomMéthode(argument)). Dans la méthode Draw, il faut retirer l'appel au Clear, car il faut afficher les couches sous-jacentes. Enfin, dans la méthode HandleInput, juste après la gestion des boutons, retirons le mouvement du joueur et remplaçons-le par le code suivant, qui permettra de mettre à jour les couches :
Commençons par la modification de la classe GamePlayScene. Elle doit désormais étendre ParallaxeScrollerScene à la place d'AbstractGameScene. On va ensuite rajouter un appel de base sur les méthodes LoadContent, UnloadContent, HandleInput et Draw (cela se fait en appelant base.nomMéthode(argument)). Dans la méthode Draw, il faut retirer l'appel au Clear, car il faut afficher les couches sous-jacentes. Enfin, dans la méthode HandleInput, juste après la gestion des boutons, retirons le mouvement du joueur et remplaçons-le par le code suivant, qui permettra de mettre à jour les couches :
foreach (var parallaxeScrollingScene in Parallaxes) parallaxeScrollingScene.MoveBy(-movement * MaxLayer * 4); |
Le dernier changement à apporter au système est la modification de la méthode PlayGameMenuItemSelected de la classe MainMenuScene du répertoire Scenes. Son comportement sera d'ajouter les différentes couches au GamePlayScene pour qu'il soient également chargés :
private void PlayGameMenuItemSelected(object sender, EventArgs e) { var gs = new GameplayScene(SceneManager); // Ajouter les couches du défilement parallaxe du plus éloigné au plus proche gs.AddLayer(new ParallaxeScrollingScene(SceneManager, "layer-0")); gs.AddLayer(new ParallaxeScrollingScene(SceneManager, "layer-1")); gs.AddLayer(new ParallaxeScrollingScene(SceneManager, "layer-2")); gs.AddLayer(new ParallaxeScrollingScene(SceneManager, "layer-3")); LoadingScene.Load(SceneManager, true, gs); } |
Il faut évidemment ajouter les images dans le projet de contenu associé au projet XNA, les images du tutoriel sont disponibles dans les sources du projet, disponibles ici.
Conclusion
Vous voilà avec tous les éléments nécessaires pour implémenter un système de défilement parallaxe complet en XNA 4.0. Il est évidemment possible de gérer le défilement vertical avec ce système en modifiant un peu la méthode MoveBy de la classe ParallaxScrollingScene.