• Medium

Free online content available in this course.

Paperback available in this course

course.header.alt.is_certifying

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 5/18/20

TP : Un jeu de collecte spatiale

Log in or subscribe for free to enjoy all this course has to offer!

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 sitepublicdomain4u.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énementiellekeydown():

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 valeurautoplayà l'attributautoplayde la balise<audio>. De même, vous affecterez la valeurloopà l'attributloopde 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-indexde la soucoupe volante est initialisée à 200 afin qu'elle soit toujours affichée en avant-plan. Les images d'identifiants#bonet#mauvaisont unz-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édisplayest initialisée ànoneafin 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éthodekeydown(). 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 variablesposXetposYavec 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 CSSleftettop.

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#bonet#mauvais

Les images#bonet#mauvaisdoivent ê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 fonctionMath.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é. SielemTypevaut 0, l'élément#bonest affiché aux coordonnées (elemX,elemY), puis l'élément#mauvaisest dissimulé :

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

Inversement, sielemTypeest différent de 0, l'élément#mauvaisest affiché aux coordonnées (elemX,elemY), puis l'élément#bonest dissimulé :

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

N'oubliez pas d'utiliser la méthodesetInterval()pour appeler de façon répétitive la fonctionafficheElements(). 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#bonet#mauvais. Définissez pour cela la fonctioncollisions().

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 variablesposXetposY:

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

L'instructionifsuivante détermine les coordonnées de l'élément (#bonou#mauvais) affiché et les stocke dans les variableselemXetelemY. Si l'élément#bonn'est pas affiché :

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

… cela signifie que l'élément#mauvaisest affiché. La chaîne « mauvais » est stockée dans la variableelemTypeet les coordonnées de l'élément#mauvaisle sont dans les variableselemXetelemY:

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

Dans le cas contraire, l'élément#bonest affiché. La chaîne « bon » est stockée dans la variableelemTypeet les coordonnées de l'élément#bonle sont dans les variableselemXetelemY:

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 écritposX+125-50+20etposY+177-116+20?

C'est vrai qu'il aurait été plus simple d'écrireposX+95etposY+81. Si j'ai indiqué trois nombres à la suite deposXet deposY, 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 àposXetposYdonnent 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,#info2et#info3en 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#boncapturés est incrémenté de 1, le score est incrémenté de 5 et l'élément#bonest 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#mauvaiscapturé est incrémenté de 1, le score est décrémenté de 5 et l'élément#mauvaisest 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 fonctionsetInterval():

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 fonctioncollisions()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#bonou de#mauvaiscapturé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 globalestopDetectionque nous initialiserons à 0 juste après la disponibilité du DOM :

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

Pourquoi utiliser une variable globale ?

La variablestopDetectionest 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 variablestopDetectionpour indiquer qu'il ne faut plus détecter de collisions. Parallèlement, pour qu'une collision soit effective, la variablestopCollisiondoit être égale à 0. Le code de la fonctioncollisions()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 variablestopDetectionlorsqu'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>
Example of certificate of achievement
Example of certificate of achievement