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.
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.
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.
On commencera donc par créer un Pong à un joueur. Une fois que ce sera fonctionnel, ajouter un autre joueur sera très simple.
// 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 );
function update(time) {
// Contrôles de la raquette1
if (UP) {
raquette1_posY = raquette1_posY - 2;
}
if (DOWN) {
raquette1_posY = raquette1_posY + 2;
}
}
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.
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 :
*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);
}
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);`
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 :
Astuce: Pour compter et afficher le score, rafraichissez-vous la mémoire avec l'atelier du [compteur d'invités]
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);
}