XNA / Monogame
Collisions multiples : astéroïdes / astéroïdes
1ère partie : Implémentation des classes Background et Asteroids
Tutoriel présenté par : Robert Gillard (Gondulzak)
Date d'écriture : 24 avril 2015
Préliminaires
Lors de notre Big Tuto Xna et plus précisément dans le jeu Aron et les Aliens, vous avez pu voir des astéroïdes générés sur le côté supérieur de notre fenêtre et se dirigeant tous vers le bas avec un léger décalage vers la droite. ![]()
Bien que ces astéroïdes pouvaient entrer en collision avec notre héros ou les ennemis, jamais ils n'entraient en interaction entre-eux. ![]()
Un peu plus tard, j'avais annoncé sur le forum mon intention de réaliser un tutoriel en C# sous Xna, relatif aux collisions astéroïdes/astéroïdes. Après de nombreux essais plus ou moins réussis, entre mon désir de continuer à programmer en C# sous Xna ou Monogame et mon intention de réaliser un Big Tuto C++ (toujours d'actualité d'ailleurs...
), me voici désormais en mesure de vous présenter ce tutoriel de collisions multiples. ![]()
Bien, assez de bla-bla, nous pouvons maintenant entrer dans le vif du sujet ! ![]()
Le projet complet (parties 1 et 2)
Par rapport à la génération d'astéroïdes d'Aron et les Aliens, j'ai quand-même dû procéder à quelques changements que je vous explique ici.
Les frames de la feuille de sprites des astéroïdes ont été redessinées en 32 x 32 pixels pour une commodité de collision cercle/cercle. Vous pourrez télécharger la nouvelle spritesheet ci-dessous, ou directement avec le projet complet. ![]()

Asteroides32x32.png
La génération des astéroïdes à l'écran se fait aléatoirement par 3 côtés de l'écran (gauche, haut et bas).
La quantité d'astéroïdes présente à l'écran ainsi que les vitesses mini et maxi de ceux-ci sont modifiables dans le programme.
Enfin, j'ai dû créer une classe AsteroidManager pour s'occuper de la gestion des différentes méthodes. ![]()
J'ajouterai pour terminer que j'ai gardé le background avec défilement d'étoiles du jeu Aron et les Aliens pour des raisons d'esthétique du projet mais je ne donnerai plus d'explications au sujet de la classe Background, je vous renvoie à ce sujet au chapitre 11 du Big Tuto Xna.
Ce tutoriel sera réalisé en deux parties sous Monogame, compilable sous Visual Studio 2013 Community mais vous pourriez bien entendu l'écrire avec un autre IDE supportant Xna.
Bien, on commence. Ouvrez l'IDE Visual Studio 2013 et cliquez sur New Project... Une nouvelle fenêtre apparaît et dans le bas de celle-ci, en face de Name : , entrez le nom de notre projet. Nommez-le SpaceAsteroids et dans la fenêtre centrale qui permet de créer des applications sous différentes plateformes, descendez le curseur jusqu'en bas et double-cliquez sur MonoGame Windows Project. Vous êtes maintenant prêts à créer le projet sous MonoGame ! ![]()

1 – La classe "Background"
Nous allons créer le background de notre fenêtre. A l'intérieur du projet, créez une nouvelle classe que vous nommerez Background. Celle-ci est connue, elle se trouve dans chaque chapitre à partir du chapitre 11 du big tuto Xna. Supprimez les quelques lignes du fichier Background.cs que vous venez de créer dans votre nouveau projet SpaceAsteroids et à la place, faites un copier-coller du fichier Background.cs du chapitre 11 du big tuto Xna. Je ne vais pas réécrire cette classe ici, et quant à ses commentaires je vous demanderai de bien vouloir vous reporter au chapitre 11 du big tuto Xna. ![]()
Et dans le fichier Background.cs vous n'oublierez pas de remplacer namespace AronAndTheAliens01 par namespace SpaceAsteroids (Attention, je vous ai à l'oeil !
)
2 – La classe "Asteroids"
Ok, vous pouvez maintenant créer la classe Asteroids. Supprimez les quelques lignes de cette classe nouvellement créée et remplacez-les par le code suivant.
Et comme d'habitude dans les tutoriels Xna, je commence à écrire le code qui sera ensuite commenté
:
|
#region Description
//MERUVIA XNA / Monogame TUTORIALS
//Projet : SpaceAsteroids (collisions multiples d'astéroïdes)
//Mise en place de la classe Asteroids
//Asteroids.cs
//Last update : 21/04/2015
#endregion
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace SpaceAsteroids
{
class Asteroids
{
#region Déclarations et initialisations
public Texture2D Texture;
private List<Rectangle> frames = new List<Rectangle>();
private int frameWidth = 0;
private int frameHeight = 0;
private int currentFrame;
private float frameTime = 0.1f;
private float timeForCurrentFrame = 0.0f;
private Color color = Color.LightGray;
private float rotation = 0.0f;
public int radius = 0;
private Vector2 location = Vector2.Zero;
private Vector2 velocity = Vector2.Zero;
#endregion Déclarations et initialisations
#region Constructeur
//Constructeur
public Asteroids( Vector2 location,
Texture2D texture,
Rectangle initialFrame,
Vector2 velocity)
{
this.location = location;
Texture = texture;
this.velocity = velocity;
frames.Add(initialFrame);
frameWidth = initialFrame.Width;
frameHeight = initialFrame.Height;
}
#endregion Constructeur
#region Propriétés publiques du sprite astéroïde
//Retourne la position du sprite et initialise son emplacement
public Vector2 Location
{
get { return location; }
set { location = value; }
}
//Retourne et initialise la vitesse et la direction du sprite
public Vector2 Velocity
{
get { return velocity; }
set { velocity = value; }
}
//Permet d'agir sur la couleur d'origine de la texture
public Color Color
{
get { return color; }
set { color = value; }
}
//Retourne et initialise une valeur de rotation
public float Rotation
{
get { return rotation; }
set { rotation = value % MathHelper.TwoPi; }
}
#endregion Propriétés publiques du sprite astéroïde
#region Propriétés publiques d'animation
//Retourne l'indice de la frame courante ou l'initialise dans sa
//plage d'indices
public int Frame
{
get { return currentFrame; }
set
{
currentFrame = (int)MathHelper.Clamp(value, 0,
frames.Count - 1);
}
}
//Retourne la valeur du temps d'affichage d'une frame
//ou initialise son temps pré-déterminé d'affichage
public float FrameTime
{
get { return frameTime; }
set { frameTime = MathHelper.Max(0, value); }
}
//Retourne la frame du rectangle source
public Rectangle SourceRect
{
get { return frames[currentFrame]; }
}
//Retourne un rectangle de destination
public Rectangle DestRect
{
get
{
return new Rectangle(
(int)location.X,
(int)location.Y,
frameWidth,
frameHeight);
}
}
//Retourne l'offset du centre d'un cercle
public Vector2 Center
{
get
{
return location +
new Vector2(frameWidth / 2, frameHeight / 2);
}
}
#endregion Propriétés publiques d'animation
#region Méthodes de détection des collisions
//Détection de collisions rectangle/rectangle
public bool IsBoxColliding(Rectangle Box2)
{
return DestRect.Intersects(Box2);
}
//Détection de collisions cercle/cercle
public bool IsCircleColliding(Vector2 Center2, float Radius2)
{
if (Vector2.Distance(Center, Center2) <
(radius + Radius2))
return true;
else
return false;
}
#endregion Méthodesde détection des collisions
#region Méthode AddFrame
//Méthode d'ajout de frames
public void AddFrame(Rectangle frameRectangle)
{
frames.Add(frameRectangle);
}
#endregion Méthode AddFrame
#region Update
//Mise à jour
public virtual void Update(GameTime gameTime)
{
float timeSinceLastFrame = (float)gameTime.ElapsedGameTime.TotalSeconds;
timeForCurrentFrame += timeSinceLastFrame;
if (timeForCurrentFrame >= FrameTime)
{
currentFrame = (currentFrame + 1) % (frames.Count);
timeForCurrentFrame = 0.0f;
}
location += (velocity * timeSinceLastFrame);
}
#endregion Update
#region Fonction de dessin
//Fonction de dessin
public virtual void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(
Texture,
Center,
SourceRect,
color,
rotation,
new Vector2(frameWidth / 2, frameHeight / 2),
1.0f,
SpriteEffects.None,
0.0f);
}
#endregion Fonction de dessin
}
}
|
Nous allons maintenant décortiquer cette classe Asteroids.
Déclarations et initialisations
L'animation des frames se fera au travers de la feuille de sprites Asteroide32x32.png.
- Texture : la texture de nos frames.
- frames : liste qui contiendra un Rectangle pour représenter chaque animation.
- currentFrame : variable qui contiendra la frame courante à un moment donné.
- frameTime : représente le temps d'affichage d'une frame (en secondes).
- timeForCurrentFrame : détermine le temps restant avant incrémentation de la frame suivante (en secondes).
- frameWidth et frameHeight : dimensions d'une frame.
- color : couleur qui va influencer la couleur d'origine de asteroide32x32.png
- rotation : rotation de l'astéroïde
- radius : représente le rayon d'un cercle virtuel inscrit dans une frame (je rappelle que les frames sont carrées, de 32 x 32 pixels). Ce rayon déterminera s'il y a collision ou non (on sait qu'il y a collision entre deux cercles de quelconques dimensions, de centre O et O' et de rayon r1 et r2 si l'expression O O' < r1 + r2 est vérifiée
).
- location : position de l'astéroïde à l'écran.
- velocity : vecteur représentant la vitesse et la direction d'un astéroïde.
Remarque : dans notre tutoriel, tous les astéroïdes seront de même dimension et donc de même rayon et nous supposerons une masse d'une unité mais rien ne vous empêcherait par la suite de créer une gestion de collisions de sprites de dimensions et de masses différentes (je vois que vous êtes déjà intéressés...). ![]()
Le Constructeur
Le constructeur de la classe Asteroids initialise les membres location, texture et velocity avec les valeurs passées en paramètres. Il initialise ensuite les variables frameWidth et frameHeight avec les valeurs reçues de l'objet Rectangle.
Propriétés publiques du sprite astéroïde
Nous ajoutons des propriétés publiques afin d'accéder aux membres de la classe Asteroids.
Les Propriétés Location, Velocity et Color retournent la valeur de leur membre par un get ou sont initialisées à l'aide d'un set. Cela est connu et ne pose aucune difficulté. ![]()
La Propriété Rotation quant-à-elle, demande quelques explications complémentaires. Nous verrons dans la classe AsteroidsManager de la seconde partie de notre tutoriel qu'un nouvel astéroïde est généré selon une rotation aléatoire comprise entre 0 et 360 degrés.
Ici, dans notre propriété Rotation, lors de l'initialisation de celle-ci, value est divisé par l'objet TwoPi de la classe MathHelper (MathHelper.TwoPi) et le reste de la division placé dans le membre rotation. Il s'agit d'un raccourci permettant de conserver cette valeur comprise entre 0 et 2*PI (Merci Xna...
).
Propriétés publiques d'animation
Dans la propriété Frame, set utilise MathHelper.Clamp() pour vérifier que la valeur entrée dans currentFrame est une valeur valide qui représente les frames de notre liste de Rectangles. En effet, notre feuille de sprites Asteroide32x32.png comporte 7 frames et ce nombre ne peut en aucun cas être dépassé sous risque de plantage du programme.
Comme l'indice de la première frame est 0, la dernière frame de la feuille aura donc 6 comme indice, soit (frames.Count – 1). ![]()
La Propriété FrameTime va permettre de définir la vitesse à laquelle l'animation doit être mise à jour en utilisant MathHelper.Max() pour s'assurer que la valeur est >= 0. Une valeur de FrameTime égale à 0 résultera en une animation qui met à jour ses frames lors de chaque cycle de mise à jour (je commence vraiment à l'aimer, moi, cette classe MathHelper, elle possède des méthodes intéressantes...
Vous devriez aller y jeter un coup d'oeil dans la doc de Xna
).
La Propriété SourceRect retourne donc la frame courante du spritesheet tandis que la propriété DestRect construit un nouveau Rectangle basé sur la position et les dimensions du Rectangle où devra se positionner le Rectangle SourceRect.
Pout terminer, la Propriété Center détermine la position des coordonnées du centre du sprite (position du membre location auquel on ajoute l'offset de la moitié de la longueur et de la hauteur du sprite
).
Méthodes publiques de gestion des collisions
Pour vérifier qu'il y a collision rectangulaire entre 2 boxes, on passe un autre Rectangle en paramètre à la méthode IsBoxColliding. La méthode Intesects de la classe Rectangle renverra true s'il y a collision et false dans le cas contraire.
La méthode IsCircleColliding accepte un vector2 en paramètre et va comparer (nous en avons parlé plus haut) la distance entre les centres de 2 cercles. Nous savons qu'il y aura collision circulaire si cette distance est < à la somme de leurs rayons respectifs. ![]()
Méthode AddFrame
La méthode AddFrame ajoute simplement une nouvelle frame à la liste des frames. Lors de la mise à jour de l'animation, la valeur de la frame sera comparée au nombre de frames déjà ajoutées et nous avons vu que c'est frames.Count qui s'en charge.
Mise à jour (Méthode Update)
Lors de la mise à jour des frames, chaque frame est initialisée à (currentFrame + 1) % (frames.Count). Il s'agit d'un raccourci qui ajoute une unité à la frame courante, divise le total par le nombre de frames dans l'animation et retourne le reste dans la variable currentFrame.
Après que currentFrame ait été mis à jour, timeForCurrentFrame est réinitialisé à 0 afin de recommencer une nouvelle boucle d'animation. ![]()
Pour terminer, la fonction Update() ajoute (velocity * elapsed) à la variable location, la variable elapsed étant initialisée comme variable temporaire dans la fonction et représente le temps passé par la mise à jour par défaut de Xna soit 1/60 seconde (= 60 fps).
Fonction de dessin (Fonction Draw)
Nous utilisons ici une fonction Draw() surchargée contenant 9 paramètres. Nous avons vu les différents types de surcharge d'une fonction Draw() au début de notre Big Tuto Xna. ![]()
Ici cependant, nous devons utiliser la rotation. Nous avons donc besoin d'identifier le centre d'un cercle par un Vector2 et celui-ci représentera le centre de la surface d'un objet astéroïde à l'écran soit new Vector2(frameWidth / 2, frameHeight / 2).
3 – La classe «SpaceAsteroids»
Bien, nous allons déjà modifier notre classe game1.cs créée automatiquement par le projet afin de pouvoir au moins afficher le background à l'écran lors de cette première partie du tutoriel. ![]()
Cependant, afin de ne pas vous faire languir trop longtemps et vous permettre de voir les astéroïdes se bousculer allégrement à l'écran, je vous propose le téléchargement complet du projet avec cette première partie (cf. ci-dessus). Les explications concernant la seconde partie du tutoriel et notamment celles de la très importante classe AsteroidsManager vous seront délivrées par la suite.
Ne voulant pas rendre ce tutoriel par trop indigeste à cause de sa longueur, j'ai donc préféré le réaliser en deux parties. ![]()
Ok, retour à notre classe SpaceAsteroids. Dans l'explorateur de solution en haut à droite de notre IDE, faites un clic droit sur le nom du fichier Game1.cs et dans la fenêtre qui apparaît double-cliquez sur Rename (renommer). Remplacez alors Game1.cs par SpaceAsteroids.cs. Répondez ensuite OUI à la question qui vous est posée.
Vous allez ensuite effacer tout le contenu de la classe SpaceAsteroids et remplacer le code par celui-ci :
|
#region Description
//MERUVIA XNA / Monogame TUTORIALS
//Projet : SpaceAsteroids (collisions multiples d'astéroïdes)
//Mise en place de la classe SpaceAsteroids
//SpaceAsteroids.cs
//Last update : 13/04/2015
#endregion Description
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 SpaceAsteroids
{
public class SpaceAsteroids : Microsoft.Xna.Framework.Game
{
#region Déclarations et initialisations
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Background background; //Création d'une instance de classe Background
Texture2D asteroidTexture; //Texture astéroïdes
// Les états du clavier qui détermineront les touches pressées
KeyboardState currentKeyboardState;
KeyboardState previousKeyboardState;
// Les états du gamepad qui détermineront les boutons pressés
GamePadState currentGamePadState;
GamePadState previousGamePadState;
#endregion Déclarations et initialisations
#region constructeur
public SpaceAsteroids()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
#endregion constructeur
#region Fonction d'initialisation
protected override void Initialize()
{
graphics.PreferredBackBufferHeight = 480;
graphics.PreferredBackBufferWidth = 800;
//graphics.IsFullScreen = true;
//graphics.ApplyChanges();
base.Initialize();
}
#endregion Fonction d'initialisation
#region Fonction LoadContent
protected override void LoadContent()
{
// Creation d'un nouveau spriteBatch
// qui sera utilisé pour le dessin des textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// Création d'une instance de Background à qui on passe le ContentManager
background = new Background(Content);
// Nous chargeons déjà notre texture astéroïde pour le chapitre suivant
asteroidTexture = Content.Load<Texture2D>("asteroide32x32");
}
#endregion Fonction LoadContent
#region Fonction UnloadContent
protected override void UnloadContent()
{
// Nettoyage de la mémoire
}
#endregion Fonction UnloadContent
#region Mise à jour
protected override void Update(GameTime gameTime)
{
// Permet la sortie du jeu
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// Sauve le précédent état du clavier et du gamepad afin que l'on puisse
// déterminer quelle touche/bouton sera pressé
previousGamePadState = currentGamePadState;
previousKeyboardState = currentKeyboardState;
// Lit l'état courant du clavier ou du gamepad et l'enregistre
currentKeyboardState = Keyboard.GetState();
currentGamePadState = GamePad.GetState(PlayerIndex.One);
background.UpdateBackground();
base.Update(gameTime);
}
#endregion Mise à jour
#region Fonction de dessin
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
background.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
#endregion Fonction de dessin
}
}
|
Je ne commenterai pas ce fichier, il n'y a rien ici que vous n'ayez déjà vu dans les chapitres du big tuto Xna. ![]()
Et voilà le service minimum de notre classe SpaceAsteroids pour afficher un background étoilé défilant à l'écran !
Mais il nous reste les dossiers de textures à ajouter à notre projet et comme nous n'en avons que trois, nous allons les ajouter toutes aujourd'hui. ![]()
Dans la fenêtre de l'explorateur de solutions créez deux nouveaux dossiers, l'un que vous nommerez Backgrounds et l'autre Textures.
Dans le dossier Backgrounds vous ajouterez les fichiers PrimaryBackground.png et ParallaxStars.png et dans le dossier Textures ajoutez le fichier Asteroide32x32.png.
Et voilà, c'est tout ? ![]()
He non, attendez...
Nous sommes sous MonoGame et cette plateforme nous oblige à effectuer un petit travail supplémentaire sans quoi nous ne verrions jamais nos textures à l'écran !! ![]()
Remarque importante : maintenant que nos textures sont dans les dossiers Backgrounds et Textures, ouvrez ces deux dossiers et cliquez sur chacune des images png. Vous allez voir une petite fenêtre s'ouvrir en dessous de la fenêtre de l'explorateur de solution. Dans cette fenêtre vous verrez une instruction « do not copy ». Cliquez alors juste à côté de ces trois mots et dans la petite fenêtre qui va s'ouvrir, cliquez sur « copy always ». Refaites ceci pour les trois textures, c'est une petite manoeuvre imposée par MonoGame qui n'était pas nécessaire sous Xna mais croyez-moi, je n'y suis pour rien... ![]()
Et voilà, ici se termine la première partie de notre tutoriel, et c'était vraiment la plus simple. ![]()
Dans quelque temps, la seconde partie vous sera proposée et il faudra vous accrocher mais je sais que vous êtes déjà prêts.
En attendant les explications de la seconde partie, vous pouvez déjà télécharger le code complet ainsi que le fichier exécutable afin de vous rendre compte du résultat de ce projet. ![]()

A bientôt pour la suite. ![]()
Gondulzak.

English
Français 
