• Facile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

TP : Un jeu de collecte spatiale

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Pour terminer cette partie, je vous propose de réaliser un jeu en jQuery. Votre mission, si vous l'acceptez, va consister à diriger un vaisseau spatial au pavé numérique et à collecter des éléments qui apparaissent de façon aléatoire sur l'écran. Mais attention, il y a deux types d'éléments : les bons (des vaches) et les mauvais (des voitures Men In Black). Les premiers ajoutent 5 points à votre score, alors que les seconds en enlèvent 5.

Instructions pour réaliser le TP

Avant de commencer, je vous propose de regarder la figure suivante pour voir à quoi ressemblera le jeu.

Le jeu une fois terminé
Le jeu une fois terminé

Les huit touches fléchées du pavé numérique permettront de diriger le vaisseau. Et vous verrez que ce n'est pas un luxe : la partie se joue très vite et le déplacement en diagonale est un vrai plus.

Pour vous faire réfléchir un peu plus, je vous demande d'ajouter une musique de fond au jeu. Celle-ci devra démarrer dès l'ouverture de la page et boucler sans fin. Vous pouvez utiliser n'importe quelle musique aux formats MP3 et OGG. Pour ma part, j'ai utilisé la musique BabyPleaseDontGo.mp3, téléchargée gratuitement sur le site publicdomain4u.com.

Vous devriez être capables d'écrire tout le code sans aucun conseil de ma part. Je vais cependant vous fournir les fichiers dont vous aurez besoin et, au passage, vous donner deux ou trois conseils qui vous aideront à partir d'un bon pied.

Voici les ressources utilisées dans ce jeu :

fond.png : 600 × 400 pixels
fond.png : 600 × 400 pixels
soucoupe.png : 125 × 177 pixels
soucoupe.png : 125 × 177 pixels
bon.png : 50 × 116 pixels
bon.png : 50 × 116 pixels
mauvais.png : 56 × 113 pixels
mauvais.png : 56 × 113 pixels

Le tableau suivant donne les codes ASCII des touches du pavé numérique. Il vous sera utile lorsque vous écrirez la procédure événementielle keydown() :

Touche

Code ASCII

Droite

39

Gauche

37

Bas

40

Haut

38

Diagonale haut et gauche

36

Diagonale haut et droite

33

Diagonale bas et gauche

35

Diagonale bas et droite

34

Dans le chapitre précédent, vous avez appris à jouer un son lorsqu'une collision se produit. Pour jouer une musique de fond, vous utiliserez le même principe, mais ici vous activerez la musique dès l'ouverture de la page en affectant la valeur autoplay à l'attribut autoplay de la balise <audio>. De même, vous affecterez la valeur loop à l'attribut loop de la balise <audio> pour que la musique boucle sur elle-même :

<audio preload="auto" id="musiqueFond" autoplay="autoplay" loop="loop">
  <source src="BabyPleaseDontGo.mp3" type="audio/mp3">
  <source src="BabyPleaseDontGo.mp3" type="audio/ogg">
</audio>

Et maintenant, la balle est dans votre camp. À vos claviers, et amusez-vous bien !

Correction

J'espère que tout s'est bien passé. Pour faciliter la correction, nous allons procéder par étapes successives. Assurez-vous que vous avez passé chaque étape avec succès et, en cas de doute, comparez le code de la correction avec votre propre code. Comme toujours en programmation, il n'y a pas une solution mais plusieurs qui donnent lieu à plusieurs codes, parfois très différents. Si votre code ne ressemble pas du tout au mien mais fonctionne, ce n'est pas grave. Cela signifie simplement que vous êtes partis dans une autre direction. Ce qui compte avant tout, c'est qu'il fonctionne.

Structure HTML et mise en forme CSS

La première étape consiste à mettre en place l'ossature HTML du document. Voici le code que j'ai utilisé :

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>jeu</title>
  <style type="text/css">
    #jeu
    {
      width: 600px;
      height: 400px;
      border: 2px black solid;
      background: url('fond.png');
    }
    #soucoupe{
      z-index: 200; 
      position: absolute; 
      top: 20px; 
      left: 70px;
    }
    #bon{
      z-index: 100; 
      position: absolute; 
      display: none;
    }
    #mauvais{
      z-index: 100; 
      position: absolute; 
      display: none;
    }
  </style>
</head>

<body>
  <div id="jeu">
    <img id="soucoupe" src="soucoupe.png">
    <img id="bon" src="bon.png">
    <img id="mauvais" src="mauvais.png">
  </div>
  Bon : <span id="info1">0</span> Mauvais : <span id="info2">0</span> Score : <span id="info3">0</span> 
  <audio preload="auto" id="musiqueFond" autoplay="autoplay" loop="loop">
    <source src="BabyPleaseDontGo.mp3" type="audio/mp3">
    <source src="BabyPleaseDontGo.ogg" type="audio/ogg">
  </audio>

  <script src="jquery.js"></script>
  <script>
    // Insérez le code jQuery ici
  </script>
</body>
</html>

Comme vous pouvez le voir, il n'y a rien d'exceptionnel dans ces lignes. L'aire de jeu correspond à la balise <div id="jeu">. Cette balise héberge trois images : l'arrière-plan (soucoupe.png), l'élément à collecter (bon.png) et l'élément qui ne doit pas être collecté (mauvais.png).

Trois informations sont affichées en dessous de l'aire de jeu :

  1. Le nombre de bons objets collectés (#info1) ;

  2. Le nombre de mauvais objets collectés (#info2) ;

  3. Le score du joueur (#info3).

Enfin, une balise <audio> est utilisée pour la musique de fond.

Le code CSS n'offre aucune difficulté. L'aire de jeu est dimensionnée, encadrée et une image y est affichée en arrière-plan. La propriété z-index de la soucoupe volante est initialisée à 200 afin qu'elle soit toujours affichée en avant-plan. Les images d'identifiants #bon et #mauvais ont un z-index égal à 100. Elles s'afficheront donc entre l'image d'arrière-plan et la soucoupe volante. Elles sont positionnées de façon absolue et leur propriété display est initialisée à none afin d'être invisibles à l'ouverture de la page.

Déplacement du vaisseau

Cette deuxième étape va donner vie aux touches du pavé numérique. Dans quelques minutes, vous pourrez déplacer le vaisseau où bon vous semble.

Voici le code utilisé :

$(document).keydown(function(e){
  if (e.which == 39) // Vers la droite
  {
    posX = parseInt($('#soucoupe').css('left'));
    if (posX < 470)
      $('#soucoupe').css('left', posX+30);
  }     
  if (e.which == 37) // Vers la gauche
  {
    posX = parseInt($('#soucoupe').css('left'));
    if (posX > 20)
      $('#soucoupe').css('left', posX-30);
  }     
  if (e.which == 40) // Vers le bas
  {
    posY = parseInt($('#soucoupe').css('top'));
    if (posY < 230)
      $('#soucoupe').css('top', posY+30);
  }     
  if (e.which == 38) // Vers le haut
  {
    posY = parseInt($('#soucoupe').css('top'));
    if (posY > 20)
      $('#soucoupe').css('top', posY-30);
  }     
  if (e.which == 36) // Vers le haut et la gauche
  {
    posX = parseInt($('#soucoupe').css('left'));
    posY = parseInt($('#soucoupe').css('top'));
    if ((posY > 20) && (posX > 20))
      $('#soucoupe').css('left', posX-30).css('top', posY-30);
  }     
  if (e.which == 33) // Vers le haut et la droite
  {
    posX = parseInt($('#soucoupe').css('left'));
    posY = parseInt($('#soucoupe').css('top'));
    if ((posY > 20) && (posX < 470))
      $('#soucoupe').css('left', posX+30).css('top', posY-30);
  }     
  if (e.which == 35) // Vers le bas et la gauche
  {
    posX = parseInt($('#soucoupe').css('left'));
    posY = parseInt($('#soucoupe').css('top'));
    if ((posX > 20) && (posY < 230))
      $('#soucoupe').css('left', posX-30).css('top', posY+30);
  }     
  if (e.which == 34) // Vers le bas et la droite
  {
    posX = parseInt($('#soucoupe').css('left'));
    posY = parseInt($('#soucoupe').css('top'));
    if ((posY < 230) && (posX < 470))
      $('#soucoupe').css('left', posX+30).css('top', posY+30);
  }
});

Ne vous laissez pas impressionner par le nombre d'instructions contenues dans la méthode keydown(). Si vous y regardez d'un peu plus près, vous verrez qu'elle contient huit blocs de code consacrés au traitement des huit touches du pavé numérique. Prenons par exemple le code traitant de la touche 9 du pavé numérique. Cette touche doit déplacer le vaisseau en diagonale, vers le haut et la droite.

if (e.which == 33) // Vers le haut et la droite
{
  posX = parseInt($('#soucoupe').css('left'));
  posY = parseInt($('#soucoupe').css('top'));
  if ((posY > 20) && (posX < 470))
    $('#soucoupe').css('left', posX+30).css('top', posY-30);
}

Les deux premières instructions initialisent les variables posX et posY avec les coordonnées de la soucoupe. Si ces coordonnées le permettent, le déplacement est effectué. Ici, la soucoupe ne doit pas être trop haut (posY > 20) ni trop à droite (posX < 470). Le déplacement s'effectue en modifiant les propriétés CSS left et top.

Une fois ces instructions saisies, vous pouvez vérifier que la soucoupe est guidée avec les touches fléchées du pavé numérique.

Rien ne se passe. Est-ce que j'ai oublié quelque chose ?

Si la soucoupe reste immobile, vérifiez que la touche Verr Num n'est pas active. Si vous utilisez un ordinateur portable, il se peut que les touches fléchées soient absentes du clavier. Dans ce cas, vous devrez choisir d'autres touches. Reportez-vous à la section traitant de la gestion événementielle du clavier, au chapitre 1 de la partie 3, pour avoir la liste des codes ASCII des touches du clavier.

Affichage des éléments #bon et #mauvais

Les images #bon et #mauvais doivent être affichées périodiquement à des positions choisies aléatoirement. Pour cela, vous devez mettre en place une fonction qui s'exécutera à intervalles réguliers. Voici le code de cette fonction :

function afficheElements()
{
  var elemX = Math.floor(Math.random()*500)+20;
  var elemY = Math.floor(Math.random()*300)+20;
  var elemType = Math.floor(Math.random()*2);
  if (elemType == 0)
  {
    $('#bon').css('top',elemY).css('left',elemX);
    $('#bon').show();
    $('#mauvais').css('display','none');
  }
  else
  {
    $('#mauvais').css('top',elemY).css('left',elemX);
    $('#mauvais').show();
    $('#bon').css('display','none');
  }
}

Les trois premières instructions utilisent la fonction Math.random() pour tirer des nombres aléatoires. Le premier correspond à l'abscisse de l'élément qui va s'afficher. Il est compris entre 0 et 519. Le deuxième correspond à l'ordonnée de l'élément qui va s'afficher. Il est compris entre 0 et 319. Enfin, le troisième détermine le type de l'élément qui sera affiché. Si elemType vaut 0, l'élément #bon est affiché aux coordonnées (elemX, elemY), puis l'élément #mauvais est dissimulé :

if (elemType == 0)
{
  $('#bon').css('top',elemY).css('left',elemX);
  $('#bon').show();
  $('#mauvais').css('display','none');
}

Inversement, si elemType est différent de 0, l'élément #mauvais est affiché aux coordonnées (elemX, elemY), puis l'élément #bon est dissimulé :

else
{
  $('#mauvais').css('top',elemY).css('left',elemX);
  $('#mauvais').show();
  $('#bon').css('display','none');
}

N'oubliez pas d'utiliser la méthode setInterval() pour appeler de façon répétitive la fonction afficheElements(). Ici, la période entre deux exécutions est fixée à 2 secondes :

setInterval(afficheElements, 2000);

Gestion des collisions

Le programme est presque terminé : il ne reste plus qu'à gérer les collisions entre le vaisseau et les éléments #bon et #mauvais. Définissez pour cela la fonction collisions().

function collisions()
{
  posX = parseInt($('#soucoupe').css('left'));
  posY = parseInt($('#soucoupe').css('top'));
  if ($('#bon').css('display') == 'none')
  {
    elemType = 'mauvais';
    elemX = parseInt($('#mauvais').css('left'));
    elemY = parseInt($('#mauvais').css('top'));
  }
  else 
     {
    elemType = 'bon';
    elemX = parseInt($('#bon').css('left'));
    elemY = parseInt($('#bon').css('top'));
  }
  if ((elemX>posX-20) && (elemX<(posX+125-50+20)) && (elemY>posY-20) && (elemY<(posY+177-116+20)) && (stopDetection == 0))
  {
    if (elemType=='bon')
    {
      var nbBon = parseInt($('#info1').text())+1;
      $('#info1').text(nbBon);
      var score = parseInt($('#info3').text())+5;
      $('#info3').text(score);
      $('#bon').css('display', 'none');
    }
    else
    {
      var nbMauvais = parseInt($('#info2').text())+1;
      $('#info2').text(nbMauvais);
      var score = parseInt($('#info3').text())-5;
      $('#info3').text(score);
      $('#mauvais').css('display', 'none');
    }
  }
}

Les deux premières instructions mémorisent les coordonnées de la soucoupe dans les variables posX et posY :

posX = parseInt($('#soucoupe').css('left'));
posY = parseInt($('#soucoupe').css('top'));

L'instruction if suivante détermine les coordonnées de l'élément (#bon ou #mauvais) affiché et les stocke dans les variables elemX et elemY. Si l'élément #bon n'est pas affiché :

if ($('#bon').css('display') == 'none')

… cela signifie que l'élément #mauvais est affiché. La chaîne « mauvais » est stockée dans la variable elemType et les coordonnées de l'élément #mauvais le sont dans les variables elemX et elemY :

elemType = 'mauvais';
elemX = parseInt($('#mauvais').css('left'));
elemY = parseInt($('#mauvais').css('top'));

Dans le cas contraire, l'élément #bon est affiché. La chaîne « bon » est stockée dans la variable elemType et les coordonnées de l'élément #bon le sont dans les variables elemX et elemY :

elemType = 'bon';
elemX = parseInt($('#bon').css('left'));
elemY = parseInt($('#bon').css('top'));

Il ne reste plus qu'à tester si la soucoupe et l'élément affiché aux coordonnées (elemX, elemY) se chevauchent :

if ((elemX>posX-20) && (elemX<(posX+125-50+20)) && (elemY>posY-20) && (elemY<(posY+177-116+20)))

Pourquoi avoir écrit posX+125-50+20 et posY+177-116+20 ?

C'est vrai qu'il aurait été plus simple d'écrire posX+95 et posY+81. Si j'ai indiqué trois nombres à la suite de posX et de posY, c'est uniquement dans un but pédagogique : le vaisseau a une largeur de 125 pixels et les éléments à collecter une largeur d'environ 50 pixels. En vérifiant que l'abscisse de l'élément est supérieure à celle du vaisseau et inférieure à celle du vaisseau + la largeur du vaisseau - la largeur de l'élément, on s'assure que l'élément est entièrement couvert par le vaisseau.

Il en va de même en ce qui concerne les deux derniers tests : en vérifiant que l'ordonnée de l'élément est supérieure à celle du vaisseau et inférieure à celle du vaisseau + la hauteur du vaisseau - la hauteur de l'élément, on s'assure que l'élément est entièrement couvert par le vaisseau.

Les 20 pixels ajoutés à posX et posY donnent une marge de sécurité autour du vaisseau afin que la collision soit plus facile à détecter. De la même façon, on enlève 20 pixels dans le premier et le troisième test pour faciliter la détection des collisions. Sans cet artifice, il faudrait que les éléments soient entièrement masqués par le vaisseau pour qu'une collision se produise.

Les lignes suivantes mettent à jour les balises <span>#info1, #info2 et #info3 en fonction de la nature de l'élément qui est entré en collision avec le vaisseau. S'il s'agit de l'élément #bon :

if (elemType=='bon')

… le nombre d'éléments #bon capturés est incrémenté de 1, le score est incrémenté de 5 et l'élément #bon est dissimulé :

var nbBon = parseInt($('#info1').text())+1;
$('#info1').text(nbBon);
var score = parseInt($('#info3').text())+5;
$('#info3').text(score);
$('#bon').css('display', 'none');

S'il s'agit de l'élément #mauvais, le nombre d'éléments #mauvais capturé est incrémenté de 1, le score est décrémenté de 5 et l'élément #mauvais est dissimulé :

else
{
  var nbMauvais = parseInt($('#info2').text())+1;
  $('#info2').text(nbMauvais);
  var score = parseInt($('#info3').text())-5;
  $('#info3').text(score);
  $('#mauvais').css('display', 'none');
}

Pour terminer, activez cette fonction à intervalles réguliers. Par exemple toutes les 200 millisecondes avec la fonction setInterval() :

setInterval(collisions, 200);

Je vous sens impatients de tester ce code. Allez-y !

Tout fonctionne correctement si ce n'est un léger problème avec la détection des collisions. Étant donné que la fonction collisions() est exécutée toutes les 200 millisecondes, le programme détecte parfois plusieurs collisions alors qu'une seule s'est produite. Ce qui provoque l'augmentation ou la diminution excessive du nombre de #bon ou de #mauvais capturés. Bien entendu, ce problème se propage jusqu'au score qui peut s'envoler ou diminuer bien plus rapidement que ce qu'il devrait !

Pour résoudre ce problème, nous allons définir la variable globale stopDetection que nous initialiserons à 0 juste après la disponibilité du DOM :

$(function() {
  var stopDetection = 0;
  ...

Pourquoi utiliser une variable globale ?

La variable stopDetection est dite « globale » car elle n'est pas liée à une méthode ou à une fonction donnée. Sa portée sera donc globale dans tout le code JavaScript.

Lorsqu'une collision est détectée, la valeur 1 est stockée dans la variable stopDetection pour indiquer qu'il ne faut plus détecter de collisions. Parallèlement, pour qu'une collision soit effective, la variable stopCollision doit être égale à 0. Le code de la fonction collisions() devient donc le suivant :

if ((elemX>posX) && (elemX<(posX+233)) && (elemY>posY) && (elemY<(posY+127)) && (stopDetection == 0))
{
  $('#son')[0].play();
  stopDetection = 1;

Il ne reste plus qu'à mettre à 0 la variable stopDetection lorsqu'un nouvel élément est affiché :

function afficheElements()
{
  stopDetection = 0;
  ...

Ça y est, le code est entièrement opérationnel. J'espère que sa mise au point ne vous a posé aucun problème. N'hésitez pas à modifier ou ajouter des choses (des sons par exemple). Ce qui serait bien, c'est de permettre au joueur de mettre la musique en pause ou en lecture. ;)

Essayer le jeu

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>jeu</title>
  <style type="text/css">
    #jeu{
      width: 600px;
      height: 400px;
      border: 2px black solid;
      background: url('fond.png');
    }
    #soucoupe{
      z-index: 200; 
      position: absolute; 
      top: 20px; 
      left: 70px;
    }
    #bon{
      z-index: 100; 
      position: absolute; 
      display: none;
    }
    #mauvais{
      z-index: 100; 
      position: absolute; 
      display: none;
    }
  </style>
</head>

<body>
  <div id="jeu">
    <img id="soucoupe" src="soucoupe.png">
    <img id="bon" src="bon.png">
    <img id="mauvais" src="mauvais.png">
  </div>
  Bon : <span id="info1">0</span> Mauvais : <span id="info2">0</span> Score : <span id="info3">0</span> 
  <audio preload="auto" id="musiqueFond" autoplay="autoplay" loop="loop"><source src="BabyPleaseDontGo.mp3" type="audio/mp3"><source src="BabyPleaseDontGo.ogg" type="audio/ogg"></audio>

  <script src="jquery.js"></script>
  <script>
    $(function() {
      var stopDetection = 0;
      $(document).keydown(function(e){
      if (e.which == 39) // Vers la droite
      {
        posX = parseInt($('#soucoupe').css('left'));
          if (posX < 470)
            $('#soucoupe').css('left', posX+30);
        }     
        if (e.which == 37) // Vers la gauche
        {
          posX = parseInt($('#soucoupe').css('left'));
          if (posX > 20)
            $('#soucoupe').css('left', posX-30);
        }     
        if (e.which == 40) // Vers le bas
        {
          posY = parseInt($('#soucoupe').css('top'));
          if (posY < 230)
            $('#soucoupe').css('top', posY+30);
        }     
        if (e.which == 38) // Vers le haut
        {
          posY = parseInt($('#soucoupe').css('top'));
          if (posY > 20)
            $('#soucoupe').css('top', posY-30);
        } 
        if (e.which == 36) // Vers le haut et la gauche
        {
          posX = parseInt($('#soucoupe').css('left'));
          posY = parseInt($('#soucoupe').css('top'));
          if ((posY > 20) && (posX > 20))
            $('#soucoupe').css('left', posX-30).css('top', posY-30);
        }     
        if (e.which == 33) // Vers le haut et la droite
        {
          posX = parseInt($('#soucoupe').css('left'));
          posY = parseInt($('#soucoupe').css('top'));
          if ((posY > 20) && (posX < 470))
            $('#soucoupe').css('left', posX+30).css('top', posY-30);
        }     
        if (e.which == 35) // Vers le bas et la gauche
        {
          posX = parseInt($('#soucoupe').css('left'));
          posY = parseInt($('#soucoupe').css('top'));
          if ((posX > 20) && (posY < 230))
            $('#soucoupe').css('left', posX-30).css('top', posY+30);
        }     
        if (e.which == 34) // Vers le bas et la droite
        {
          posX = parseInt($('#soucoupe').css('left'));
          posY = parseInt($('#soucoupe').css('top'));
          if ((posY < 230) && (posX < 470))
            $('#soucoupe').css('left', posX+30).css('top', posY+30);
        }     
      });

      function afficheElements()
      {
        stopDetection = 0;
        var elemX = Math.floor(Math.random()*500)+20;
        var elemY = Math.floor(Math.random()*300)+20;
        var elemType = Math.floor(Math.random()*2);
        if (elemType == 0)
        {
          $('#bon').css('top',elemY).css('left',elemX);
          $('#bon').show();
          $('#mauvais').css('display','none');
        }
        else
        {
          $('#mauvais').css('top',elemY).css('left',elemX);
          $('#mauvais').show();
          $('#bon').css('display','none');
        }
      }

      function collisions()
      {
        posX = parseInt($('#soucoupe').css('left'));
        posY = parseInt($('#soucoupe').css('top'));
        if ($('#bon').css('display') == 'none')
        {
          elemType = 'mauvais';
          elemX = parseInt($('#mauvais').css('left'));
          elemY = parseInt($('#mauvais').css('top'));
        }
        else         
        {
          elemType = 'bon';
          elemX = parseInt($('#bon').css('left'));
          elemY = parseInt($('#bon').css('top'));
        }
        if ((elemX>posX-20) && (elemX<(posX+125-50+20)) && (elemY>posY-20) && (elemY<(posY+177-116+20)) && (stopDetection == 0))
        {
          stopDetection = 1;
          if (elemType=='bon')
          {
            var nbBon = parseInt($('#info1').text())+1;
            $('#info1').text(nbBon);
            var score = parseInt($('#info3').text())+5;
            $('#info3').text(score);
            $('#bon').css('display', 'none');
          }
          else
          {
            var nbMauvais = parseInt($('#info2').text())+1;
            $('#info2').text(nbMauvais);
            var score = parseInt($('#info3').text())-5;
            $('#info3').text(score);
            $('#mauvais').css('display', 'none');
          }
        }                         
      }
       
      setInterval(afficheElements, 2000);
      setInterval(collisions, 200);
    });
  </script>
</body>
</html>
Exemple de certificat de réussite
Exemple de certificat de réussite