Pong

Étape 4
Étape terminée ?

Le célèbre jeux où vous tapez dans la balle avec une raquette. Dans cette tape nous allons aborder:

  • Les collisions avancées

  • On va remplacer nos rectangles par des sprites par ce que bon, c'est plus beau, non ?

  • Une révision de tout ce que vous venez d'apprendre pour pogrammer un Pong à 2 joueurs.

Défiez vos amis avec un jeu 2 joueurs. Vous allez construire avec tout ce qu'on a vu jusqu'à présent, et approfondir vos connaissances sur les conditions if.

Pong, un grand classique du jeu vidéo

Durée 30-40 minutes (et plus si affinité)

Prérequis

Pong était un des premiers jeux vidéo d'arcade en 1972 (Très bonne année du reste mais c'est une autre histoire...), et a connu un énorme succès. Ce classique du jeu vidéo reste à la fois amusant et suffisamment simple pour permettre d'apprendre à coder.

Dans cette étape, nous allons recréer notre version de ce classique. J'insiste sur le 'notre version' car vous avez le pouvoir. En le programmant, vous ne subissez plus un jeu conçu par quelqu'un pour vous mais vous créer le jeu et vous le paramétrer comme vous le souhaitez. Pour y parvenir, on utilisera ce que l'on a vu dans les atelier précédents pour faire bouger la balle et les raquettes et nous allons y ajouter la gestion des collisions. Mais commençons par faire une analyse plus détaillée du jeu.

La décomposition du jeu

J'ai décomposé le jeu en plusieurs parties. Cela nous permettra de nous concentrer sur une seule tâche à la fois. De plus, essayer de comprendre comment un jeu est programmé en regardant son ensemble peut-être intimidant. En le décomposant, on voit qu'en réalité le jeu est composé de plusieurs parties simples.

  • Créer la balle et la raquette
  • Mettre à jour la position de la raquette
  • Mettre à jour la position de la balle
  • Vérifier les collisions entre la balle et les murs
  • Vérifier les collisions entre la balle et la raquette
  • Afficher la balle et la raquette
  • Compter et afficher les scores
  • Vérifier si la partie est gagné ou au contraire si elle est perdue
  • Ajouter un second joueur

On commencera donc par créer un Pong à un joueur. Une fois que ce sera fonctionnel, ajouter un autre joueur sera très simple.

Implémenter la balle et la raquette

// Caractéristiques de la balle
let balle_posX = 20;
let balle_posY = 20;
let balle_speedX = 1;
let balle_speedY = 1;
let balle_taille = 4;


// Caractéristiques de la raquette
let raquette1_posX = 10;
let raquette1_posY = 30;


// Dimensions de la raquette
let raquette_hauteur = 18;
let raquette_largeur = 3;


function init() {
    setFont(R.fontKarateka); 
}

function update(time) {

}

function render() {
    setPen(0,0,0);
    clear();
}

Ici on a créé nos deux objets : la balle et la première raquette. La balle a une position horizontale (X) et une position verticale (Y). Pour chaque axe on a aussi une vitesse qui détermine la direction de la balle. La raquette a elle aussi une position X et Y. Cependant elle n'a pas de vitesse car on la contrôlera avec les flèches. La raquette a une hauteur et une largeur (raquette_hauteur et raquette_largeur). On a pareil pour la balle, sauf que la balle est un carré, donc on se contentera de balle_taille pour définir sa largeur et hauteur.

function render() {
  setPen(0,0,0);
  clear();

  // Afficher la balle
  setPen(200,150,100); // Gamebuino Brown
  rect(balle_posX, balle_posY, balle_taille, balle_taille);
	
  // Afficher la raquette
  setPen(190,190,190); // Light gray
  rect(raquette1_posX, raquette1_posY, raquette_largeur, raquette_hauteur);
}

Maintenant, on affiche notre balle et notre raquette. Rien de trop nouveau ici, on rappelle juste la syntaxe de rect:

rect( coordonnée x du rectangle , coordonnée y du rectangle , largeur , hauteur );

Contrôler la raquette

function update(time) {
  // Contrôles de la raquette1
  if (UP) {
    raquette1_posY = raquette1_posY - 2;
  }

  if (DOWN) {
    raquette1_posY = raquette1_posY + 2;
  }
}

Ajouter le mouvement de la balle

function update(time) {
  // Contrôles de la raquette1
 ...

  // Contrôles le déplacement de la balle

  balle_posX = balle_posX + balle_speedX;
  balle_posY = balle_posY + balle_speedY;


  if (balle_posY < 0) {  // Rebond en haut
    balle_speedY = 1;
  }

  if (balle_posY > getHeight() - balle_taille) {  // Rebond en bas
    balle_speedY = -1;
  }

  if (balle_posX > getWidth() - balle_taille) {  // Rebond à droite
    balle_speedX = -1;
  }

}

Ici, on applique ce qu'on a vu précédemment. On met à jour la position de la balle. Ensuite on vérifie les rebonds avec les murs. La différence avec la dernière fois est qu'on n'a plus que 3 murs : en haut, en bas et à droite. Le côté gauche doit être "défendu" par la raquette. Plus tard, quand on aura mis le second joueur, on enlèvera aussi le mur droit.

Si vous jouez au jeu tel quel, la balle se déplace, la raquette aussi. Mais la balle passe tout droit à travers la raquette ! Pas très amusant, non ? Alors regardons comment gérer les rebonds sur la raquette.

Gérer la collision et le rebond

Contrairement aux collisions avec les bords de l'écran, la raquette se déplace. On doit donc vérifier plusieurs conditions pour pouvoir dire si la balle va rebondir. Pour ça, on va s'aider du schéma ci dessous. Vous pouvez y voir la raquette en noir et 3 positions possibles pour la balle. Dans ces 3 cas, on veut que la balle rebondisse.

On va en déduire 3 conditions nécessaires:

  • Tout d'abord, vous remarquez qu'un rebond ne se passe que si la balle touche la raquette. Plus précisément, si le côté gauche de la balle touche le côté droit de la raquette dans notre exemple.

  • Mais pour que la balle touche au moins partiellement la raquette, il faut que le côté bas de la balle soit en DESSOUS du côté haut de la raquette.

  • De la même manière, en regardant le troisième cas, il faut aussi que le haut de la balle soit au-DESSUS du bas de la raquette.

En résumé, il y a rebond :

  • Si le côté gauche de la balle est sur le côté droit de la raquette
  • Si le côté bas de la balle est plus bas que le haut de la raquette
  • Si le côté haut de la balle est plus haut le bas de la raquette

*Des conditions complexes comme celles-ci son presque impossibles à déduire sans l'aide d'un schéma comme le diagramme ci-dessus. Aurélien avait fait ce diagramme sur un bout de papier avec un crayon (puis il en a fait une image propre pour ce tutoriel :P ) mais comme lui, lorsque vous programmez un jeu, n'hesitez pas à utiliser une feuille et un crayon pour écrire ou dessiner les choses et vous aider à les visualiser et à les transcrire en code. Ça vous évitera des maux de tête. *

Si ces 3 conditions sont valides, il y a donc collision et rebond de la balle sur la raquette. Alors dans ce cas, on change la direction de la balle. Pour l'implémenter, on pourrait écrire des if imbriqués :

if (balle_posX == raquette1_posX + raquette_largeur) {  // Si le côté gauche de la balle est sur le côté droit de la raquette
  if (balle_posY + balle_taille >= raquette1_posY) {  // Si le côté bas de la balle est plus bas que le haut de la raquette
    if (balle_posY <= raquette1_posY + raquette_hauteur) {  // Si le côté haut de la balle est plus haut que le bas de la raquette
       // Rebond
    }
  }
}

Placer une série de if l'un dans l'autre crée beaucoup d'accolades, et c'est assez lourd visuellement. En C/C++ on peut simplifier ce genre de "cascade" de if avec les opérateurs ET et OU. L'opérateur ET s'écrit && et l'opérateur OU s'écrit || (Pour le caractère | il faut faire AltGr+6 sur un clavier Windows, option + maj + L pour un clavier Mac). Voici un petit exemple :

// Si a vaut 3 ET b est négatif
if ((a == 3) && (b < 0)) {
}

// Si a est supérieur ou égale à 3 OU si b est égale à 0
if ((a >= 3) || (b == 0)) {
}

Il faut toujours une paire de parenthèses ( ) autour de l'ensemble des conditions.

Dans notre cas, on teste 3 conditions. Mais que ça soit pour 2, 3, ou 12 conditions, avec les opérateurs && et ||, on peut toutes les mettre ensemble. Mais si vous mettez les conditions sur la même ligne, on se retrouve avec une ligne très longue! Heureusement pour nous, on peut retourner à la ligne entre 2 conditions :

// Les trois conditions sur la même ligne
if ( (balle_posX == raquette1_posX + raquette_largeur) && (balle_posY + balle_taille >= raquette1_posY) && (balle_posY <= raquette1_posY + raquette_hauteur) ) {
    // Rebond
}

// Les mêmes conditions avec des retours à la ligne
if ( (balle_posX == raquette1_posX + raquette_largeur) 
  && (balle_posY + balle_taille >= raquette1_posY)
  && (balle_posY <= raquette1_posY + raquette_hauteur) ) {
  // Rebond
}

Nous avons maintenant une manière d'interagir avec la balle. Quand le joueur rate la balle et qu'elle sort de l'écran, il a perdu. Il faut replacer la balle pour commencer une nouvelle manche.

  // Contrôles de la raquette //
  // Mise à jour de la balle + collisions //
	...

// Collision balle/raquette
  if (balle_posX < 0) {
    // Remettre la balle au milieu de l'écran
    balle_posX = 20;
    balle_posY = 20;
    balle_speedX = 1;
    balle_speedY = 1;
  }

  // Afficher la balle et la raquette //
}

Voilà, maintenant quand la balle sort par le côté gauche, on la replace dans l'écran et on la fait partir par la droite pour ne pas surprendre le joueur ;) Je vous remets tout le code que nous avons fait jusqu'à présent :

// Caractéristiques de la balle
let balle_posX = 20;
let balle_posY = 20;
let balle_speedX = 1;
let balle_speedY = 1;
let balle_taille = 4;


// Caractéristiques de la raquette
let raquette1_posX = 10;
let raquette1_posY = 30;


// Dimensions de la raquette
let raquette_hauteur = 18;
let raquette_largeur = 3;


function init() {
  setFont(R.fontKarateka); 
}

function update(time) {
  // Contrôles de la raquette1
  if (UP) {
    raquette1_posY = raquette1_posY - 2;
  }

  if (DOWN) {
    raquette1_posY = raquette1_posY + 2;
  }


  // Contrôles le déplacement de la balle

  balle_posX = balle_posX + balle_speedX;
  balle_posY = balle_posY + balle_speedY;


  if (balle_posY < 0) {  // Rebond en haut
    balle_speedY = 1;
  }

  if (balle_posY > getHeight() - balle_taille) {  // Rebond en bas
    balle_speedY = -1;
  }

  if (balle_posX > getWidth() - balle_taille) {  // Rebond à droite
    balle_speedX = -1;
  }

  // Collision balle/raquette
  if ( (balle_posX == raquette1_posX + raquette_largeur)
    && (balle_posY + balle_taille >= raquette1_posY) 
    && (balle_posY <= raquette1_posY + raquette_hauteur) ) {
    balle_speedX = 1;
  }

  // Vérifier si la balle est sortie de l'écran
  if (balle_posX < 0) {
    // Replacer la balle sur l'écran
    balle_posX = 20;
    balle_posY = 20;
    balle_speedX = 1;
    balle_speedY = 1;
  }

}

function render() {
  setPen(0,0,0);
  clear();
    
  // Afficher la balle
  setPen(200,150,100); // Gamebuino Brown
  rect(balle_posX, balle_posY, balle_taille, balle_taille);
  
  // Afficher la raquette
  setPen(190,190,190); // Light gray
  rect(raquette1_posX, raquette1_posY, raquette_largeur, raquette_hauteur);
}

Et si on remplaçait ces rectangles par des sprites ?

Bon, on ne s'emballe pas, un sprite, c'est juste un rectangle dans lequel on a plusieurs couleurs... et de la transparence. Alors on a une balle de taille 4, on va donc faire un sprite de 4 pixels par 4 pixels. Pour garder la transparence, on va utiliser le format PNG et on le sauvegardera en 32 bits... Pour le dessin de l'image, avec 4 pixels on ne pourra pas vraiment laisser court à notre créativité, je vous propose donc de faire une image monochrome et comme c'est pour un tuto pour la Meta, je vais la faire de la couleur du Orange 'Gamebuino' (Sprite à la taille réelle: ) Faisons la même chose pour la raquette (On a 18 pixel de haut et 3 de large):

Maintenant que nous avons nos 2 images, on va les ajouter dans notre programme. Vous allez voir, c'est on ne peut plus simple: Vous ouvrez votre explorateur de fichiers et vous faites glisser vos images dans la partie images (encadrée en rouge sur l'image ci dessous)

Maintenant, on remplace nos instruction rect par l'instruction image(texture:ImageResource, X:number, Y:number)qui va dessiner le sprite à la position désignée par X et Y. A savoir que pour indiquer le nom de l'image, on écrit R.nom de l'image. Par exemple pour notre sprite de balle, nous l'avons appelé ball.png, on l'a ajouté dans la partie images, pour l'appeler via l'instruction image, j'écrirais: ìmage(R.ball, X, Y);`

À vous de jouer!

Nous avons fait la décomposition du jeu vidéo Pong, puis nous avons recréé le cœur du jeu. Une balle se déplace sur l'écran, et le joueur déplace une raquette. S'il laisse passer la balle derrière lui, la partie est perdue, et la balle se replace sur l'écran. Je vous laisse compléter les éléments du jeu suivants :

  • Ajouter une deuxième raquette
  • Utiliser les boutons A et B pour la contrôler
  • Détecter quand la balle sort de l'écran par la droite
  • Compter le score
  • Afficher le score

Astuce: Pour compter et afficher le score, rafraichissez-vous la mémoire avec l'atelier du [compteur d'invités]

Exemple de solution

Si vous êtes en panne d'inspiration, voilà ce que l'on a fait de notre côté :)

// Caractéristiques de la balle
let balle_posX = 20;
let balle_posY = 20;
let balle_speedX = 1;
let balle_speedY = 1;
let balle_taille = 4;

// Dimensions des raquettes
let raquette_hauteur = 18;
let raquette_largeur = 3;
let vitesse_raquettes = 2;

// Scores
let score1;  // Score du joueur 1
let score2;  // Score du joueur 2

// Caractéristiques des raquettes
let raquette1_posX, raquette1_posY, raquette2_posX, raquette2_posY;

let largeur_ecran, hauteur_ecran



function init() {
  setFont(R.fontKarateka); 

  // Initialisation des informations générales
  largeur_ecran = getWidth();
  hauteur_ecran = getHeight();

  // Caractéristiques des raquettes
  raquette1_posX = 10;
  raquette1_posY = (getHeight() - raquette_hauteur)/2;
  raquette2_posX = largeur_ecran - 10 - raquette_largeur;
  raquette2_posY = (getHeight() - raquette_hauteur)/2;

  // Initialisation des score
  score1 = 0;
  score2 = 0;
}

function relance_balle() {
  balle_posX = 20;
  balle_posY = 20;
  balle_speedX = 1;
  balle_speedY = 1;
}

function update(time) {
  // Contrôles de la raquette1
  if (UP) {
    raquette1_posY = raquette1_posY - vitesse_raquettes;
  }

  if (DOWN) {
    raquette1_posY = raquette1_posY + vitesse_raquettes;
  }


  // Contrôles de la raquette2
  if (A) {
    raquette2_posY = raquette2_posY - vitesse_raquettes;
  }

  if (B) {
    raquette2_posY = raquette2_posY + vitesse_raquettes;
  }

  // Contrôles le déplacement de la balle

  balle_posX = balle_posX + balle_speedX;
  balle_posY = balle_posY + balle_speedY;


  if (balle_posY < 0) {  // Rebond en haut
    balle_speedY = 1;
  }

  if (balle_posY > getHeight() - balle_taille) {  // Rebond en bas
    balle_speedY = -1;
  }


  // Collision balle/raquette
  if ( (balle_posX == raquette1_posX + raquette_largeur)
    && (balle_posY + balle_taille >= raquette1_posY) 
    && (balle_posY <= raquette1_posY + raquette_hauteur) ) {
    balle_speedX = 1;
  }

    // Collision balle/raquette2
  if ( (balle_posX + balle_taille == raquette2_posX)
    && (balle_posY + balle_taille >= raquette2_posY) 
    && (balle_posY <= raquette2_posY + raquette_hauteur) ) {
    balle_speedX = -1;
  }


  // Vérifier si la balle est sortie de l'écran
  if (balle_posX < 0) {
    relance_balle();        // Replacer la balle sur l'écran
    score2 = score2 + 1;    // Incrémenter le score du joueur 2
  }

  if (balle_posX > largeur_ecran) {
    relance_balle();      // Replacer la balle sur l'écran
    score1 = score1 + 1;  // Incrémenter le score du joueur 1
  }

}

function render() {
  setPen(0,0,0);
  clear();
  
  // Afficher les scores
  setPen(255,255,255); // White
  text(score1 + " - " + score2, 70, 5); 

  // Avant d'afficher des sprites, remetre le crayon en noir
  setPen(0,0,0);
    
  // Afficher la balle
  image(R.ball, balle_posX, balle_posY);
  
  // Afficher les raquettes
  image(R.pad, raquette1_posX, raquette1_posY);
  image(R.pad, raquette2_posX, raquette2_posY);
  }

Étapes