Pong en javascript – graphisme moderne

Douzième partie consacrée au développement du jeu vidéo pong html5 javascript : habillage moderne. Le jeu Pong change de look, le look actuel est oldschool. Je vous propose donc de le moderniser facilement.

 

Prérequis pour un habillage moderne

Avoir lu les tutoriaux consacrés à l’initialisation du projet Coder le jeu vidéo Pong. 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. L’animation de la balle Coder le jeu vidéo Pong – Animer la balle et au contrôle de la raquette par le joueur à l’aide du clavier Coder le jeu vidéo Pong – Animer les raquettes. Le contrôle de la raquette par le joueur à l’aide de la souris Coder le jeu vidéo Pong – Controle à la souris. Le renvoi de la balle par les raquettes Coder le jeu video pong – Renvoi de la balle. La gestion des effets sonores Coder le jeu video pong – Ajout des effets sonores. La gestion de l’IA Coder le jeu vidéo Pong – Intelligence artificielle. La gestion du score et de l’engagement Coder le jeu vidéo Pong – Score et engagement. La gestion des parties Coder le jeu vidéo Pong – Début et fin de partie. La gestion de la vitesse de la balle Coder le jeu vidéo Pong – Vitesse et trajectoire.

 

Le principe pour un habillage moderne

Jusqu’à présent, seuls des éléments graphiques simples ont été utilisés. Des raquettes à base de rectangles blancs, une balle sous la forme d’un carré tout simple. A cela s’ajoute des polices de caractères par défaut.

Pour ce qui est du réagencement global, cela fera l’objet d’un autre article.

Vous allez procéder au changement de look et intégrez.
– des éléments graphiques à base d’images pour les raquettes et la balle : avec de vrais sprites;
– un décor digne de ce nom avec un agencement revu et corrigé;
– l’utilisation d’une police de caractères personnalisée pour l’affichage du score;
– la modification du bouton de démarrage du jeu.

Voici l’écran duquel on part.

Les changements

Voici les modifications à opérer pour un look modernisé.

1 – Les raquettes et la balle vont être remplacées par des images;
2 – le bouton start game va être remplacé par un bouton sous forme d’image;
3 – la police de caractères de l’affichage du score sera une police personnalisée.

 

Les raquettes et la balle

L’opération consiste ici à remplacer les rectangles blancs représentant raquettes et balle par des images. Libre à vous ensuite de relooker vos raquettes comme bon vous semble. Il suffira de modifier l’image.

Je vous propose les 3 images suivantes pour respectivement le joueur 1, le joueur 2 et la balle.

La première chose à faire est de créer un dossier img au sein de l’arborescence projet dans lequel seront stockées les images.

Ensuite, vous devez associer ces 3 images à leurs objets respectifs: playerOne, playerTwo et ball du namespace javascript.

Une nouvelle propriété (imagePath) à ajouter à chaque objet dont la valeur est le chemin d’accès au fichier image.

const game = {
  ....
  ball : {
    width : 10,
    height : 10,
    color : "#FFD700",
    posX : 200,
    posY : 200,
    directionX : 1,
    directionY : 1,
    speed : 1,
    inGame : false,
    imagePath : "./img/ball.png"
    ....
  },
 
  playerOne : {
    width : 10,
    height : 50,
    color : "#FFFFFF",
    posX : 30,
    posY : 200,
    goUp : false,
    goDown : false,
    originalPosition : "left",
    score : 0,
    ai : false,
    imagePath : "./img/playerOne.png"
  },
     
  playerTwo : {
    width : 10,
    height : 50,
    color : "#FFFFFF",
    posX : 650,
    posY : 200,
    goUp : false,
    goDown : false,
    originalPosition : "right",
    score : 0,
    ai : true,
    imagePath : "./img/playerTwo.png"
  },
  ....
}

Ceci fait, vous devez afficher ces images en lieu et place des rectangles blancs pour les raquettes et du carré jaune pour la balle.

Pour les joueurs, une fonction du namespace game est dédiée à leur affichage.

const game = {
  ....
  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);
  },
  ....
}

Cette même fonction fait appel à la fonction drawRectangleInLayer du namespace game.display qui dessine un rectangle avec une couleur donnée à un endroit donné.

Ici plutôt que de dessiner des rectangles, dessinez des images. D’où l’ajout d’une fonction de tracé d’image dans ce même namespace game.display.

game.display = {
  ....
  drawImageInLayer : function(targetLayer, image, x, y) {
    ....
  },
  ....
}

Mais afficher une image nécessite de disposer d’un objet Image. Html5 permet de dessiner des images dans un canvas à partir de son contexte 2D via la fonction drawImage qui prend en paramètre un objet image.

Vous êtes donc obligé d’initialiser un objet Image à partir du chemin du fichier de l’image imagePath ajouté en propriété des objets playerOne, playerTwo et ball.

Dans l’ordre :
– ajouter une nouvelle propriété img à chacun des 3 objets;
– initialiser cet objet à partir du chemin du fichier imagePath.

Initialiser l’objet Image nécessite d’ajouter une nouvelle méthode initImage à chacun des 3 objets.

initImage : function(width, height) {
  this.img = new Image(width, height);
  this.img.src = this.imagePath;
  this.img.width = width;
  this.img.height = height;
}

N’oubliez pas de l’ajouter aux 3 objets playerOne, playerTwo et ball. Clairement, c’est une duplication de code et donc une mauvaise pratique.

Je ne souhaite pas partir dans du refactoring de code à cet instant. Cela vous ferez sortir du sujet principal et du but, ça n’apporterait que confusion. Je vous propose de réaliser cette opération à la fin de cette partie.

Appelez la fonction initImage pour chacun des objets depuis la fonction init du namespace game.

const game = {
  ....
  init : function() {
    ....
    this.displayScore(0,0);
   
    this.ball.initImage(10,10);
    this.displayBall(200,200);
   
    this.playerOne.initImage(15,70);
    this.playerTwo.initImage(15,70);
    this.displayPlayers();
    ....
  },
  ....
}

Il reste à coder la fonction drawImageInLayer du namespace game.display.

game.display = {
  ....
  drawImageInLayer : function(targetLayer, image, x, y) {
    targetLayer.context2D.drawImage(image, x, y);
  }
  ....
}

Puis à modifier la fonction displayPlayers du namespace game.

const game = {
  ....
  displayPlayers : function() {
    game.display.drawImageInLayer(this.playersBallLayer, this.playerOne.img, this.playerOne.posX, this.playerOne.posY);
    game.display.drawImageInLayer(this.playersBallLayer, this.playerTwo.img, this.playerTwo.posX, this.playerTwo.posY);
  },
  ....
}

Même chose avec la fonction displayBall du namespace game.

const game = {
  ....
  displayBall : function() {
    game.display.drawImageInLayer(this.playersBallLayer, this.ball.img, this.ball.posX, this.ball.posY);
  },
  ....
}

Et voilà le résultat.

Le refactoring

Pour un petit rappel sur le modèle objet de javascript, je vous invite à lire ou relire les articles suivants.
Les namespaces et objets javascript;
5 manières de créer des objets javascript.

Javascript a un modèle objet dit littéral différent du modèle objet par classe comme le langage java par exemple.

Que remarque-t-on à la relecture du code ? Non seulement qu’il y a des objets qui portent des propriétés identiques mais aussi que ces propriétés portent aussi le même sens.

A regarder de plus près, les objets playerOne, playerTwo et ball du namespace game, le partage d’un point de vue sens, des propriétés suivantes est flagrant.
– la largeur width;
– la hauteur height;
– la position sur l’axe X posX;
– la position sur l’axe Y posY;
– pour la représentation graphique, le chemin vers le fichier image imagePath et l’objet image img.

Regrouper ces propriétés dans un objet dédié a du sens. Ca ressemble presque à des propriétés de sprite.

Où placer cet objet au sein des différents namespace javascript ? Vous vous êtes attachés à créer du code qui soit réutilisable avec notamment le namespace game.display.

La bonne idée serait de continuer cette volonté de réutilisation. Le sprite étant un élément graphique, le placer dans le namespace game.display n’est pas déconnant.

Ce qui donne dans le namespace game.display.

game.display = {
  ....
  sprite : {
    width : 0,
    height : 0,
    posX : null,
    posY : null,
    imagePath : "",
    img : null
  },
 
  createSprite : function(width, height, posX, posY, imagePath) {
    let sprite = Object.create(this.sprite);
    
    sprite.width = width;
    sprite.height = height;
    sprite.posX = posX;
    sprite.posY = posY;
    sprite.imagePath = imagePath;
    sprite.img = new Image();
    sprite.img.src = imagePath;
    sprite.img.width = width;
    sprite.img.height = height;
 
    return sprite;
  }
}

L’objet sprite avec ses propriétés est ajouté ainsi qu’un cloneur de cet objet (createSprite).

Ensuite substituez un sprite aux propriétés communes dans les objets playerOne, playerTwo et ball, les propriétés width, height, posX, posY, imagePath et img disparaissent au profit de l’objet sprite.

Supprimez aussi la méthode initImage dont le contenu a été transféré dans le cloneur createSprite.

const game = {
  ....
  ball : {
    sprite : null,
    color : "#FFD700",
    directionX : 1,
    directionY : 1,
    speed : 1,
    inGame : false,
    ....
  },
  ....
  playerOne : {
    sprite : null,
    color : "#FFFFFF",
    goUp : false,
    goDown : false,
    originalPosition : "left",
    score : 0,
    ai : false
  },
     
  playerTwo : {
    sprite : null,
    color : "#FFFFFF",
    goUp : false,
    goDown : false,
    originalPosition : "right",
    score : 0,
    ai : true
  },
}

Les propriétés appartiennent désormais à l’objet sprite encapsulé dans les objets playerOne, playerTwo et ball. Il ne reste plus qu’à modifier le code où les propriétés width, height, posX, posY sont utilisées en les substituant respectivement à sprite.width, sprite.height, sprite.posX, sprite.posY.

Certes, un peu fastidieux, mais c’est le prix de la qualité et de la réutilisabilité.

Le bouton démarrage du jeu

Le bouton très moche startGame est à remplacer par l’image suivante.

La première chose à faire est de remplacer le bouton par l’image.

<input id="startGame" type="image" src="./img/startBtn.png">

Tel quel, l’image est à sa taille originelle, il faut donc la redimensionner comme ceci.

<input id="startGame" style="width:120px" type="image" src="./img/startBtn.png">

Le résultat.

Et il n’y a que ça à faire.

 

La police de caractères du score pour un habillage moderne

L’objectif est d’intégrer une police de caractères personnalisée pour l’affichage du score. La police choisie pour l’exemple.

Vous pouvez la télécharger en cliquant DS_DIGITAL. Il suffit ensuite de décompresser le fichier et de copier le dossier décompressé dans le dossier font du projet à créer s’il n’existe pas.

Pour l’utiliser, il est indispensable de la déclarer par le biais du mot clé css @font-face comme suit.

<style>
@font-face { 
  font-family: 'DS-DIGIB'; 
  src: url('./font/ds_digital/DS-DIGIB.TTF'); 
}
</style>

où :
– font-family donne un nom d’usage à utiliser dans le jeu vidéo Pong;
– src indique où se trouve le fichier ttf de la police de caractères.

Pour utiliser cette police, il suffit de la spécifier DS-DIGIB dans la propriété font-family du style.

font-family: DS-DIGIB;

C’est au niveau du contexte du canvas html5 qu’il faut opérer. Par le biais de la propriété font et remplaçer dans la fonction displayScore.

const game = {
  ....
  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);
  },
  ....
}

par

const game = {
  ....
  displayScore : function(scorePlayer1, scorePlayer2) {
    game.display.drawTextInLayer(this.scoreLayer, scorePlayer1, "50pt DS-DIGIB", "#FFFFFF", this.scorePosPlayer1, 55);
    game.display.drawTextInLayer(this.scoreLayer, scorePlayer2, "50pt DS-DIGIB", "#FFFFFF", this.scorePosPlayer2, 55);
  },
  ....
}

Vous obtenez.

Apprenez à créer un jeu vidéo en une soirée.

Accessible à tous.

Nombre de places limité.

Voir l'événement

Bravo, jette un œil à ta boite mail pour télécharger ton guide.