Coder le jeu vidéo html5 pong – Animer les raquettes

 

Quatrième partie consacrée au développement d’un jeu vidéo html5 javascript. L’objectif du jour est d’animer la raquette du joueur par le biais des touches du clavier.


 

Prérequis

Avoir lu les tutoriaux consacrés à l’initialisation du projet Coder le jeu vidéo Pong 1ere partie, à la mise en place de l’environnement du jeu constitué du terrain, du filet, des raquettes, de la balle et du score Coder le jeu vidéo Pong – Raquettes et Balle et à l’animation de la balle Coder le jeu vidéo Pong – Animer la balle.

 

Le principe

Celui-ci va consister à bouger la raquette du joueur :
– vers le haut lorsqu’il appuiera sur la touche flèche haut;
– vers le bas lorsqu’il appuiera sur la touche flèche bas.

Javascript met à disposition du développeur un mécanisme qui permet de détecter s’il y a eu appui sur une touche du clavier. Ce mécanisme passe par le biais des événements :
onkeydown permet de détecter lorsqu’une touche est pressée;
onkeyup permet de détecter lorsqu’une touche est relevée après avoir été pressée.

Ces deux événements ne distinguent pas les touches sur lesquelles l’utilisateur a appuyé : cette partie doit être gérée par le développeur.

 

Les touches de contrôle de la raquette

Souvenez vous de l’instruction :

window.onload = initialisation; // appel de la fonction initialisation au chargement de la page

Elle associe à l’événement onload (chargement de la page) une fonction initialisation chargée d’initialiser le jeu.

Pour les événements onkeydown> et onkeyup, le principe est identique : nous allons leur associer à chacun une fonction Javascript de manière construite.

Il se peut que vous retrouviez souvent dans vos lectures ce type de déclaration :

// association des méthodes aux évènements :
// onKeyDown = à l'appui de la touche
// onKeyUp = au relèvement de la touche
window.onkeydown = onKeyDown;
window.onkeyup = onKeyUp;

Pour structurer le code, je vous propose de créer un nouveau namespace dédié au contrôle dans un fichier dédié game.control.js en n’oubliant de l’intégrer comme source dans le fichier game.html.

game.control = {
}

Ce namespace va encapsuler les fonctions qui seront appelées sur l’appui sur les touches.

game.control = {
  onKeyDown : function(event) {
  },  
  onKeyUp : function(event) {
  }
}

L’association événement/fonction est confié au namespace racine game.js par le biais d’une nouvelle fonction que nous appelons initKeyboard :

var game = {
...
  initKeyboard : function(onKeyDownFunction, onKeyUpFunction) {
    window.onkeydown = onKeyDownFunction;
    window.onkeyup = onKeyUpFunction;
  }
};

En l’état, il n’y a pas d’association puisque la fonction initKeyboard n’est pas appelée. Cette fonction n’a vocation à être appelée qu’une seule fois lors de l’initialisation et donc depuis la fonction dédiée init :

  init : function() {
    ...
    this.displayScore(0,0);
    this.displayBall(200,200);
    this.displayPlayers();
	
    this.initKeyboard(game.control.onKeyDown, game.control.onKeyUp);
	
  },

La structure étant en place, il ne reste plus qu’à l’utiliser pour le jeu vidéo Pong que nous développons.

Les fonctions game.control.keyDown et game.control.keyUp en l’état réagissent à l’appui sur n’importe quelle touche du clavier.

Pour que seules les touches flèche haut et flèche bas soient les seules réactives pour le jeu, il est nécessaire d’appliquer en quelque sorte un filtre.

Ce filtre va simplement consister à tester si l’une de ces deux touches est appuyée notamment par le biais du code de la touche.

L’objet event des fonctions onKeyDown et onKeyUp comporte diverses propriétés parmi lesquelles figure le code de la touche sélectionnée (event.keycode).

Le code correspondant à :
– la touche flèche haut est 38;
– la touche flèche bas est 40.

Toujours pour une meilleure lisibilité, je vous propose d’encapsuler ces valeurs dans un namespace dédié game.keycode.js en n’oubliant de l’intégrer comme source dans le fichier game.html :

game.keycode = {

  KEYDOWN : 40,
  KEYUP : 38
}

Il ne reste plus qu’à intégrer le test des touches dans les fonctions onKeyDown et onKeyUp :

game.control = {
  
  onKeyDown : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) {
    } else if ( event.keyCode == game.keycode.KEYUP ) {
    }
  },

  onKeyUp : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) {
    } else if ( event.keyCode == game.keycode.KEYUP ) {
    }
  }
}

 

Le mouvement de la raquette

Pour donner un mouvement à la raquette, il vous suffit de modifier les coordonnées (posX et posY) de l’objet game.playerOne.

Dans le cas présent, la raquette ne bougeant que verticalement, seule son ordonnée va évoluer.

C’est donc la valeur de la variable game.playerOne.posY qui va changer :
– pour que la raquette monte, sa valeur doit diminuer;
– pour que la raquette descende, sa valeur doit augmenter.

Cependant, pour éviter que la raquette sorte de l’écran par le haut ou par le bas, la valeur de game.playerOne.posY doit avoir une limite basse et une limite haute. La limite basse ne peut être inférieure à 0, et la limite haute ne peut être supérieure à la largeur du terrain (groundHeight) à laquelle on soustrait la taille de la raquette (game.playerOne.height).

Il vient naturellement à l’esprit d’incrémenter et de décrémenter la valeur de game.playerOne.posY dans la fonction onKeyDown :

game.control = {
  
  onKeyDown : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) {
      playerOne.posY+=5;
    } else if ( event.keyCode == game.keycode.KEYUP ) {
      playerOne.posY-=5;
    }
  },

  onKeyUp : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) {
    } else if ( event.keyCode == game.keycode.KEYUP ) {
    }
  }
}

Et d’y ajouter les tests de dépassement d’écran :

game.control = {
  
  onKeyDown : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN && game.playerOne.posY < game.groundHeight - game.playerOne.height ) {
      playerOne.posY+=5;
    } else if ( event.keyCode == game.keycode.KEYUP && game.playerOne.posY > 0 ) {
      playerOne.posY-=5;
    }
  },

  onKeyUp : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) {
    } else if ( event.keyCode == game.keycode.KEYUP ) {
    }
  }
}

En l’état le mouvement de la raquette fonctionne. Toutefois, visuellement parlant, cela manque de fluidité et si vous tentez de bouger la raquette vers le haut puis immédiatement vers le bas, vous constatez un certain temps de latence pour passer d’un mouvement à l’autre.

L’explication est très simple : les évènements liés à l’appui sur les touches (et donc la modification de la position de la raquette) sont désynchronisés de l’affichage. En d’autres termes, le détection de l’appui sur une touche ne se fait pas en même temps que l’affichage (dans le même cycle).

Pour rappel, un cycle d’affichage correspond à un passage dans la fonction main.

La solution consiste donc à synchroniser les mouvements de la raquette avec l’affichage en ne modifiant la position de la balle qu’une fois par cycle d’affichage. Pour cela, il suffit de créer une fonction movePlayers chargée de modifier la position de la raquette en modifiant la valeur de game.playerOne.posY et qui sera appelée par la fonction principale juste avant l’appel de la fonction displayPlayers.

Cette solution nécessite de modifier les fonctions onKeyDown et onKeyUp de manière à ce qu’elle positionne deux indicateurs. L’un indiquant que la touche flèche haut a été pressée et l’autre indiquant que la touche flèche bas a été pressée.

Commençons par créer les indicateurs goUp et goDown. Ceux-ci relevant du joueur, je propose de les intégrer dans les objets game.playerOne et game.playerTwo :

var game = {
  ...
  playerOne : {
    width : 10,
    height : 50,
    color : "#FFFFFF",
    posX : 30,
    posY : 200,
    goUp : false,
    goDown : false
  },
   
  playerTwo : {
    width : 10,
    height : 50,
    color : "#FFFFFF",
    posX : 650,
    posY : 200,
    goUp : false,
    goDown : false
  },
  ...
};

Puis modifiez les fonctions onKeyDown et onKeyUp :

game.control = {
  
  onKeyDown : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) {
      game.playerOne.goDown = true;
    } else if ( event.keyCode == game.keycode.KEYUP ) {
      game.playerOne.goUp = true;
    }
  },

  onKeyUp : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) {
      game.playerOne.goDown = false;
    } else if ( event.keyCode == game.keycode.KEYUP ) {
      game.playerOne.goUp = false;
    }
  }
}

Remarquez que dans la première version, la fonction onKeyUp n’était pas utilisée.

Lorsque game.playerOne.goDown ou game.playerOne.goUp sont positionnées à true, si rien ne les repositionne à false, la raquette continuera son mouvement même lorsque les touches auront été relâchées.

La fonction onKeyUp relative à l’évènement d’une touche relevée après avoir été pressée repositionne, selon le cas, à false les variables game.playerOne.goDown ou game.playerOne.goUp.

Il ne reste plus qu’à implémenter la fonction movePlayers pour qu’elle incrémente game.playerOne.posY lorsque l’indicateur game.playerOne.goDown est positionné. De même pour qu’elle décrémente game.playerOne.posY lorsque l’indicateur game.playerOne.goUp est positionné.

var game = {
  ...
  movePlayers : function() {
    if (game.playerOne.goUp && game.playerOne.posY > 0)
      game.playerOne.posY-=5;
    else if (game.playerOne.goDown && game.playerOne.posY < game.groundHeight - game.playerOne.height)
      game.playerOne.posY+=5;
  },
  ...
}

Puis de l’appeler depuis la fonction main :

  var main = function() {
    // le code du jeu
    game.clearLayer(game.playersBallLayer);
    game.movePlayers();
    game.displayPlayers();
    game.moveBall();
    requestAnimId = window.requestAnimationFrame(main); // rappel de main au prochain rafraichissement de la page
  }
}

Vous avez maintenant une raquette qui répond au doigt et à l’oeil de manière fluide.
Pour voir en live le code, cliquez sur Pong

Le prochain article sera consacrée au contrôle de la raquette à la souris, à suivre ici.

 

Si vous avez aimé cet article, partagez le.

 

Si vous constatez des coquilles, ou avez des remarques à faire ou encore souhaitez manifester votre satisfaction de ce tuto, n’hésitez pas les commentaires sont faits pour ça.

 
Le source javascript complet :
game.html

<html>
 <body>
 </body>
<script src="game.js"></script>
<script src="game.display.js"></script>
<script src="game.keycode.js"></script>
<script src="game.control.js"></script>
<script>
(function () {
  // début du code isolé
  var requestAnimId;
   
  var initialisation = function() {
    // le code de l'initialisation
    game.init();
    requestAnimId = window.requestAnimationFrame(main); // premier appel de principale au rafraichissement de la page
  }
   
  var main = function() {
    // le code du jeu
    game.clearLayer(game.playersBallLayer);
	game.movePlayers();
    game.displayPlayers();
    game.moveBall();
    requestAnimId = window.requestAnimationFrame(main); // rappel de main au prochain rafraichissement de la page
  }
   
  window.onload = initialisation; // appel de la fonction initialisation au chargement de la page
   
  // fin du code isolé
})();
</script> 
</html>

game.js

var game = {
  
  groundWidth : 700,
  groundHeight : 400,
  netWidth : 6,
  groundColor : "#000000",
  netColor : "#FFFFFF",
 
  groundLayer : null,  
  scoreLayer : null,
  playersBallLayer : null,
   
  scorePosPlayer1 : 300,
  scorePosPlayer2 : 365,
   
  ball : {
    width : 10,
    height : 10,
    color : "#FFD700",
    posX : 200,
    posY : 200,
    directionX : 1,
    directionY : 1,
    speed : 1,

    move : function() {
      this.posX += this.directionX * this.speed;
      this.posY += this.directionY * this.speed;
    },
	
    bounce : function() {
      if ( this.posX > game.groundWidth || this.posX < 0 )
        this.directionX = -this.directionX;
      if ( this.posY > game.groundHeight || this.posY < 0  )
        this.directionY = -this.directionY;			
			
    }
  },
   
  playerOne : {
    width : 10,
    height : 50,
    color : "#FFFFFF",
    posX : 30,
    posY : 200,
	goUp : false,
	goDown : false
  },
   
  playerTwo : {
    width : 10,
    height : 50,
    color : "#FFFFFF",
    posX : 650,
    posY : 200,
	goUp : false,
	goDown : false
  },
   
  init : function() {
    this.groundLayer= game.display.createLayer("terrain", this.groundWidth, this.groundHeight, undefined, 0, "#000000", 0, 0); 
    game.display.drawRectangleInLayer(this.groundLayer, this.netWidth, this.groundHeight, this.netColor, this.groundWidth/2 - this.netWidth/2, 0);
   
    this.scoreLayer = game.display.createLayer("score", this.groundWidth, this.groundHeight, undefined, 1, undefined, 0, 0);
    game.display.drawTextInLayer(this.scoreLayer , "SCORE", "10px Arial", "#FF0000", 10, 10);
   
    this.playersBallLayer = game.display.createLayer("joueursetballe", this.groundWidth, this.groundHeight, undefined, 2, undefined, 0, 0);  
    game.display.drawTextInLayer(this.playersBallLayer, "JOUEURSETBALLE", "10px Arial", "#FF0000", 100, 100);
   
    this.displayScore(0,0);
    this.displayBall(200,200);
    this.displayPlayers();
	
    this.initKeyboard(game.control.onKeyDown, game.control.onKeyUp);
	
  },
   
  displayScore : function(scorePlayer1, scorePlayer2) {
    game.display.drawTextInLayer(this.scoreLayer, scorePlayer1, "60px Arial", "#FFFFFF", this.scorePosPlayer1, 55);
    game.display.drawTextInLayer(this.scoreLayer, scorePlayer2, "60px Arial", "#FFFFFF", this.scorePosPlayer2, 55);
  },
   
  displayBall : function() {
    game.display.drawRectangleInLayer(this.playersBallLayer, this.ball.width, this.ball.height, this.ball.color, this.ball.posX, this.ball.posY);
  },

  moveBall : function() { 
    this.ball.move();
	this.ball.bounce();
    this.displayBall();
  }, 
  
  movePlayers : function() {
    if (game.playerOne.goUp && game.playerOne.posY > 0)
      game.playerOne.posY-=5;
    else if (game.playerOne.goDown && game.playerOne.posY < game.groundHeight - game.playerOne.height)
      game.playerOne.posY+=5;
  },
  
  displayPlayers : function() {
    game.display.drawRectangleInLayer(this.playersBallLayer, this.playerOne.width, this.playerOne.height, this.playerOne.color, this.playerOne.posX, this.playerOne.posY);
    game.display.drawRectangleInLayer(this.playersBallLayer, this.playerTwo.width, this.playerTwo.height, this.playerTwo.color, this.playerTwo.posX, this.playerTwo.posY);
  },

  clearLayer : function(targetLayer) {
	targetLayer.clear();
  },
  
  initKeyboard : function(onKeyDownFunction, onKeyUpFunction) {
    window.onkeydown = onKeyDownFunction;
    window.onkeyup = onKeyUpFunction;
  }
  
};

game.display.js

game.display = {
  container : "",
  
  layer : {
    name : "",
    canvas : "",
    context2D : "",
    posX : null,
    posY : null,
    width : "",
    height : "",
    backgroundColor : "",
    zIndex : "",
	
    clear : function() {
      this.context2D.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
  },
  
  createLayer : function(name, width, height, htmlContainer , zIndex, backgroundColor, x, y) {
    var layer = Object.create(this.layer);
  
    layer.canvas = window.document.createElement("canvas");
  
    layer.canvas.id = name;
  
    if ( backgroundColor != undefined ) {
      layer.canvas.style.background = backgroundColor;
      layer.backgroundColor = backgroundColor;
    }
      
    layer.zIndex = zIndex
    layer.canvas.style.zIndex = zIndex;
  
    layer.width = width
    layer.canvas.width = width;
  
    layer.height = height
    layer.canvas.height = height;
  
    if ( x != undefined )
      layer.posX = x;
  
    if ( y != undefined )
      layer.posY = y;
  
    layer.canvas.style.position = "absolute";
   
    if ( x != undefined )
      layer.canvas.style.left = x;
    if ( y != undefined )
      layer.canvas.style.top = y;
     
    if ( htmlContainer != undefined ) {
      htmlContainer.appendChild(layer.canvas);
    } else {
      document.body.appendChild(layer.canvas);
    }
 
    layer.context2D = layer.canvas.getContext('2d');
    return layer;
  },
 
  drawRectangleInLayer : function(targetLayer, width, heigth, color, x, y) {
    targetLayer.context2D.fillStyle = color;
    targetLayer.context2D.fillRect (x, y, width, heigth);
  },
 
  drawTextInLayer : function(targetLayer, text, font, color, x, y) {
    targetLayer.context2D.font = font;
    targetLayer.context2D.fillStyle = color;
    targetLayer.context2D.fillText(text, x, y);
  }
  
}

game.control.js

game.control = {
  
  onKeyDown : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) { 
      game.playerOne.goDown = true;
    } else if ( event.keyCode == game.keycode.KEYUP ) {
      game.playerOne.goUp = true;
    }
  },
  
  onKeyUp : function(event) {
    if ( event.keyCode == game.keycode.KEYDOWN ) {
      game.playerOne.goDown = false;
    } else if ( event.keyCode == game.keycode.KEYUP ) {
      game.playerOne.goUp = false;
    }
  }

}

game.keycode.js

game.keycode = {

  KEYDOWN : 40,
  KEYUP : 38
}

 

Posté dans html5, pongTaggé développer pong, pong html5, pong javascript, tutoriel html5, tutoriel jeu vidéo, tutoriel pong  |  13 commentaires

13 réponses à "Coder le jeu vidéo html5 pong – Animer les raquettes"

Répondre