XNA : Gestion des ennemies et des collisions

Lors des précédents tutoriels nous avons mis en place l’environnement de développement (XNA ou MonoGame) et nous avons commencé de découvrir XNA et MonoGame avec la création du mini jeu de shoot. Pour le moment on ne peux pas vraiment parler de jeu car on ne peu que déplacer un sprite sur l’écran, aujourd’hui nous allons ajouter des ennemies et permettre au joueur de détruire ces ennemies. Lorsqu’un ennemie sera touché par le laser, ce dernier sera effacé de l’écran (il sera mort) et enfin si le vaisseau se fait touché par un ennemie alors on arrêtera la partie et on recommencera. Je ne vais pas comme à chaque fois donner tout le code car celui-ci commence d’être gros pour un simple billet de blog, mais je vais détailler certaines parties essentielles, vous pourrez toujours télécharger le projet entier à la fin de l’article 😉 Bon vous êtes prêt à continuer votre initiation à XNA ? Alors c’est partie !

Au passage j’en profite pour vous annoncer que pour ce tutoriel j’ai changé l’image du vaisseau donc ne vous étonnez pas si vous n’avez pas la même chose en reprenant l’ancien projet 😉 Et parce qu’il faut bien rendre à César ce qui est à César, toutes les ressources graphiques utilisées pour ce tutoriel ont été réalisées par mon frère Maxime Comte, il nous donne son accord pour les utiliser dans nos développements.

 

La structure Rectangle

Qu’est ce qu’un rectangle ? Regardez un de vos Sprite, par exemple le vaisseau, vous pouvez voir que sa texture est rectangulaire, et bien un rectangle c’est ça 😉 c’est aussi une structure dans XNA qui représente votre image.

Nous allons modifier la classe Sprite pour lui ajouter une propriété Rectangle qui fera référence à une structure du même type. On a déjà toutes les informations concernant le rectangle de notre sprite avec sa position et sa taille, ma la structure Rectangle va nous être essentielle quand nous voudrons tester si un Sprite entre en collision avec un autre, on utilisera pour cela la méthode Intersects() qui va prendre en paramètre un autre rectangle, elle va renvoyer true si les deux rectangles se chevauchent !

// Définie un rectangle aux coordonnées 0, 0
// De taille 100x100
Rectangle monRectangle = new Rectangle(0, 0, 100, 100);

Une structure Rectangle est caractérisée par sa position X et Y ainsi que par sa taille Width et Height. La taille n’évolue pas souvent, par contre la position change souvent, dans le cas de notre vaisseau à chaque déplacement les coordonnées X et Y du rectangle vont changer et prendre les mêmes valeurs que celles de sa position. Voilà une capture d’écran qui résume ce dernier point :

Fonctionnement de la structure Rectangle
Fonctionnement de la structure Rectangle

Alors commençons par ouvrir la classe Sprite et ajoutons les propriétés suivantes

protected Rectangle _rectangle;

public Rectangle Rectangle
{
  get { return _rectangle; }
  set { _rectangle = value; }
}

Maintenant il faut instancier la structure quand toutes les informations dont nous avons besoin pour la créer sont disponible. Il faut donc attendre que la texture soit chargée pour avoir, la position et la taille du sprite. Vous noterez le cast en entier (int) des valeurs de la position et de la taille.

public virtual void LoadContent(string textureName)
{
  _texture = Content.Load<Texture2D>(textureName);
  _rectangle = new Rectangle(
    (int)_position.X,
    (int)_position.Y,
    (int)_texture.Width,
    (int)_texture.Height);
}

La méthode Update() va cette fois ci être utilisée car comme je l’ai déjà dit plus haut, la valeur du rectangle qui englobe le sprite est définie grâce à la position de ce dernier, si il ne bouge pas il n’y a pas de problème, mais si il bouge alors il faut recalculer à chaque déplacement sa valeur.

public virtual void Update(GameTime gameTime)
{
  _rectangle = new Rectangle(
    (int)_position.X,
    (int)_position.Y,
    (int)_texture.Width,
    (int)_texture.Height);
}

La modification de la classe est maintenant terminée 😉 il ne faudra pas oublier d’appeler maintenant la méthode Update() de chaque Sprite à chaque mise à jour sous peine d’avoir un rectangle complètement faux.

Dans la classe SpaceGame (celle qui dérive de Game) nous ajouterons donc les 2 appels suivant dans la méthode Update()

#region Mise à jour des rectangles du vaisseau et du laser
ship.Update(gameTime);
laser.Update(gameTime);
#endregion

Voilà cette fois c’est bon, à chaque mise à jour nous aurons nos bonnes coordonnées !

 

Ajouter des ennemies

Les ennemis seront représentés par des robots ou des pseudo aliens, leurs déplacement sera très simple car ils apparaîtront en haut de l’écran et descendront vers le bas, seule leur position en X sera aléatoire (oui on ne va pas les faire apparaître tous au même endroit car ça ne servirait à rien ^^).

La première chose à faire va être de créer une liste contenant tous les ennemies, toutes les secondes on en ajoutera un avec une position en X aléatoire, on va aussi faire en sorte que la texture et la vitesse de déplacement soit différent pour varier un peu les choses. On repasse sur la classe SpaceGame (c’était normalement déjà le cas) et on ajoute le code suivant :

// Compteur pour ajouter des enemies
long spawnTime;
// Enemies
List<Sprite> robots;
// Compteur pour ajouter des enemies
long spawnTime;
// Fin du jeu ?
bool finish;

Vous remarquerez que j’ai ajouté une variable booléenne qui indique si le jeu est terminé ou pas, elle nous sera utile quand le joueur se fera toucher par un ennemie, si c’est le cas on réinitialisera le jeu. La variable spawnTime va permettre de savoir depuis combien de temps un ennemie a été ajouté.

Il ne faut pas non plus oublier d’instancier la liste dans la constructeur

robots = new List<Sprite>();

On va créer maintenant une méthode privée qui devra remplir la liste avec un nouveau robot, c’est dans cette méthode que nous allons définir :

  • La position en X ;
  • La texture sera définie (Robot ou Alien) ;
  • La vitesse de chute vers le bas de l’écran.

Le tout avec l’utilisation de la classe Random pour générer un nombre aléatoire.

private void SpawnRobot()
{
    // Nouveau robot
    Sprite robot = new Sprite(this);

    // Texture "aléatoire"
    Random rand = new Random();
    if (rand.Next(3) % 2 == 0)
        robot.LoadContent("robot/robotnormal");
    else
        robot.LoadContent("robot/spacealien");

    // On lui attribut une position aléatoire sur X
    // Pour ne pas qu'il sorte de l'écran à droite la valeur max de random doit être
    // égale à la taille de l'écran - la largeur de la texture du robot
    int posX = rand.Next(graphics.PreferredBackBufferWidth - robot.Width);
    // Le robot n'est pas visible et descent du haut vers le bas
    robot.Position = new Vector2(posX, -robot.Height);

    // Plusieurs vitesse de défilement son possibles
    if (rand.Next(6) % 5 == 0)
        robot.Speed = new Vector2(0, 4);
    else
        robot.Speed = new Vector2(0, 2);

    robots.Add(robot);
}

L’objet rand permet de récupérer un nombre aléatoire compris entre 0 et le paramètre passé. Pour le choix de la texture on test si le nombre aléatoire est multiple de 2, si c’est le cas c’est la texture de robot qui est chargée sinon  c’est celle de l’alien. La position en Y est définie à -robot.Height car nous ne voulons pas voir le robot apparaitre d’un coup à l’écran alors on le place d’abord hors de l’affichage et comme ça quand il déscendra il apparaitra petit à petit 😉 Pour la position X et la vitesse de déplacement on utilise encore l’objet rand qui nous génère un nouveau nombre aléatoire à chaque fois.

Petite note sur la position en X : L’énemie doit être placé dans la zone d’affichage donc on doit fixer des limites au nombre aléatoire. Les bornes limites sont donc :

  • 0 pour le minimum
  • Largeur de l’écran – largeur du Sprite (sinon il sort)

La méthode Initialize() va elle aussi changer un peu car on va l’utiliser pour initialiser l’état du jeu (ce que l’on fait déjà actuellement) mais aussi et surtout pour remettre le jeu à zéro (quand le joueur se fera toucher par un monstre). Il faut donc vider la liste des ennemies dans cette méthode :

protected override void Initialize()
{
  // Autre initialisations
  robots.Clear();
  spawnTime = 0;
  // Determine si le jeu doit être réinitialisé
  isFinish = false;
}

Maintenant il ne reste plus qu’à lancer la méthode SpawnRobot() toutes les secondes et tester les collisions entre le joueur et les ennemies.

 

Gestion du temps et des collisions

La variable spawnTime reçoit le temps écoulé depuis la dernière mise à jour, ce temps est récupéré via la propriété ElapsedGameTime.Milliseconds de l’objet gameTime. Si la valeur est supérieur ou égale à 1000 (1 seconde == 1000 millisecondes) alors on ajoute un nouvel ennemie et on remet à 0 la variable spawnTime, ainsi toutes les 1 seconde on aura un nouvel ennemie à l’écran 🙂

// Toutes les 5 secondes on ajoute un robot
spawnTime += gameTime.ElapsedGameTime.Milliseconds;
if (spawnTime > 1000)
{
    SpawnRobot();
    spawnTime = 0;
}

Il reste 3 choses à faire, il faut tester les collisions, vérifier si le joueur à tiré, que le laser a touché un ennemie et enfin vérifier que le joueur n’a pas été touché. Tout va se passer dans la méthode Update()

// Pour chaque robot dans la collection
foreach (Sprite robot in robots)
{
  // On met à jour le rectangle de chaque robot
  robot.Update(gameTime);

  // Si le robot sort de l'écran on ne l'affiche plus et non le met plus à jour
  if (robot.Position.Y >= graphics.PreferredBackBufferHeight)
      robot.Active = false;
  else
      robot.Position = new Vector2(robot.Position.X, robot.Position.Y + robot.Speed.Y);

  #region Mise à jour des collisions
  // Si un robot touche le vaisseau on arrete tout et on réinitialise l'écran
  if (robot.Rectangle.Intersects(ship.Rectangle))
      finish = true;	

  // Si un laser touche un robot il disparait
  if (laser.Rectangle.Intersects(robot.Rectangle))
    robot.Active = false;
  #endregion
}
#endregion

On boucle sur chaque Sprite de la liste et on met à jour la position du rectangle, ensuite on test si le robot est sortie de l’écran, si c’est le cas on le désactive comme ça il n’est plus affiché. Enfin grâce à la méthode Intersects() on vérifie que le vaisseau n’entre pas en collision avec un des robot, si c’est le cas la variable isFinish passe à true et le jeu sera relancé. Enfin si un laser touche un robot on le désactive.

Commenter gérer le fait qu’un ennemie ai touché le vaisseau ? On l’a géré dans la boucle de collision avec la variable finish cependant on ne pouvais pas réinitialiser tout le jeu à partir de la boucle. En fait pour faire propre il ne faudrait pas utiliser de foreach mais plutôt un while avec plusieurs conditions d’arrêt, mais ce n’est pas le but aujourd’hui, les optimisations seront pour plus tard 😉

// Si la partie est terminée on réinitialise le tout
if (finish)
    Initialize();

En fin de méthode on ajoute ce test qui va réinitialiser tout le jeu si le joueur a été touché. Enfin on affiche le tout avec la méthode Draw() et notre travail est fini 🙂

// On dessine chaque robot actif
foreach (Sprite robot in robots)
    robot.Draw(spriteBatch);

Conclusion

Notre mini jeu est terminé et on va pouvoir maintenant passer à une grande phase de refactoring ce qui va permettre d’ajouter des fonctionnalités incontournables dans un jeu comme le son, la musique, les menus, etc… Il y a beaucoup de choses à voir alors n’hésitez pas à bien relire les tutoriels avant de passer à la suite car le plus amusant va bientôt commencer.

  • Télécharger le projet MonoGame version Windows
  • Télécharger le projet MonoGame version Linux
  • Télécharger le projet XNA pour Windows