Exercice 01 - PONG
Bonjour,
Avec ce premier jeu, j'inaugure une série d'exercices initialement réalisés en ActionScript pour Flash (vous pouvez retrouver toute la série ici : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/) que je souhaite vous proposer en HTML. Quand je dis “HTML”, j'englobe en fait plusieurs méthodes et frameworks qu'il sera possible de découvrir, mais généralement le tout est basé sur le couple HTML5 + Javascript (peu importe les libs ou le framework).
Mon objectif est de rester au plus près de ce que j'avais déjà fait en AS pour Flash, garder les mêmes raisonnements, algorithmes et astuces, puis traduire le tout en Javascript (ici le HTML5 ne sera utile que pour sa balise CANVAS).
Car oui, qu'on se le dise, HTML n'est pas un langage de programmation , tout repose donc sur Javascript, et on a de la chance, car Javascript et Actionscript sont issus de la même souche ECMAscript… ce sont donc deux langages vraiment très proches. Bon, on va pas non plus se voiler la face, Actionscript c'était… heuu.. c'est ! c'est ! désolé… quand même vachement bien, avec Javascript on a souvent l'impression de repartir quelques années en arrière, mais bon, c'est un avis tout personnel
Et là je vais m'adresser à ceux qui viennent de l'Actionscript… (pour les autres sautez ce paragraphe). Javascript et Actionscript ont une approche assez différente mais le langage est sensiblement le même, au cours de ces exercices je vais utiliser des frameworks et des libs qui tentent de s'approcher au plus près de notre cher AS3, ce qui change c'est la structure, et pour ça on va commencer tout tranquillement avec ce PONG. Si vous ne l'avez pas déjà fait et que vous avez une culture AS3, commencez par le tuto pour Flash, puis revenez pour vous convertir doucement à JS.
Cette fois c'est pour tout le monde… Je n'ai pas l'intention de reprendre toutes les bases des langages et de la programmation, il vous faudra donc un minimum de bagage pour suivre cet exercice (et non tutoriel), merci de jeter un oeil au pré-requis Je vous recommande aussi de lire avant tout le tutoriel version AS3 (ici : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/exercice_-_le_pong ), j'y explique tout un tas de petites astuces et raccourcis pour écrire son code, que je ne compte pas expliquer de nouveau ici.
Avant de commencer nos révisions, jouons un peu ( Ha non, on m'indique en régie que ce n'est pas possible* ) pour voir à quoi tout ceci va ressembler.
*pour des raisons de sécurité il n'est pas possible de vous présenter le jeu au sein d'une page du wiki, reportez-vous à la source située en bas de cette page pour voir comment ça marche.
Etude préliminaire
Tout d'abord le PONG c'est quoi ? (merci Wikipedia)
Pong est un jeu vidéo inspiré du “tennis de table” développé par Ralph Baer et son équipe à Sanders Associates en 1967.
Après y avoir joué lors d'une première démonstration en mai 1972, Nolan Bushnell, créateur de la société Atari, en fait une version améliorée : Pong.
Puisque le nom Ping-Pong est déjà une marque déposée, ils l'ont simplement appelé Pong. C'est le premier jeu vidéo à connaître un succès populaire, mais la forme la plus ancienne d'un jeu électronique de ping-pong remonte à un jeu jouable sur un oscilloscope, créé par William A. Higinbotham au laboratoire national de Brookhaven en 1958. Son jeu était intitulé “Tennis for Two”. Le concept original de Pong est une simulation simpliste de tennis de table (ping-pong).
Au tennis de table, les joueurs se tiennent de chaque côté d'une table et manient une raquette pour frapper une petite balle qui se déplace entre eux dans les deux sens. Une petite balle, se déplace à travers l'écran, rebondissant sur les rebords du haut et du bas, et les deux joueurs commandent chacun une raquette, la faisant glisser verticalement entre les extrémités de l'écran à l'aide des contrôles. Si la balle frappe la raquette, elle rebondit vers l'autre joueur. Si elle manque la raquette, l'autre joueur marque un point. La balle rebondit de différentes manières selon la façon dont elle touche la raquette. Pong peut être joué seul, la raquette opposée est alors commandée par la machine; ou à deux joueurs, chacun commandant une raquette. Sur les bornes d'arcade la raquette est habituellement commandée par un bouton rotatif (un paddle), répondant avec une vitesse variable selon la façon dont le joueur la tourne.
Son code ultra simple est à la portée de tous, mais cache également, si l'on gratte un peu la surface, des astuces spécifiques à la réalisation de jeux vidéo, c'est pourquoi tout apprenti développeur de jeu DOIT commencer par essayer de créer un simple PONG avant de tenter de placer la barre plus haut. Mais créer un PONG est à la portée de tous avec un peu de patience, alors ce qui va nous intéresser ici c'est comment le créer en une soixantaine de lignes de code, un bloc compact où il y a peu à manger, mais du concentré qu'il faudra prendre son temps pour digérer.
Les pré-requis
Pour ce programme vous devez connaître :
Variables et fonctions en Javascript : http://forums.mediabox.fr/wiki/tutoriaux/javascript/language/notions_base
Ecouteurs d'événements en Javascript : https://developer.mozilla.org/fr/docs/Web/API/EventTarget/addEventListener
Si vous souhaitez plus de précisions sur ces points, je vous encourage à parcourir le Wiki de Mediabox où vous trouverez de nombreux tutoriaux détaillés.
La structure
Le support principal est une page HTML classique utilisant une simple balise canvas et intégrant une feuille de style et le script du jeu.
<!DOCTYPE html> <html> <head> <title>Pong</title> <link rel="stylesheet" type="text/css" href="css/styles.css" /> <script type="text/javascript" src="js/jeu.js"></script> <!--[if lt IE 9]><script type="text/javascript" src="excanvas.compiled.js"></script><![endif]--> </head> <body> <canvas id="canvas">Votre navigateur ne supporte pas HTML5.</canvas> </body> </html>
Pour faire simple, dans la balise <head> on intégre la feuille de style et le script du jeu, et dans le <body> on crée une balise <canvas> qui va servir d'écran d'affichage pour le jeu. Canvas est en fait une zone de dessin, pour les flasheux c'est un peu comme un SWF, on va dessiner notre jeu dedans… Pour l'arborescence des dossiers vous faites comme vous le souhaitez, moi j'ai créé un dossier par type de fichiers, je vous recommande de le faire pour plus de clarté. J'ai pour ma part un dossier CSS dans lequel je met mon fichier styles.css, un dossier ASSETS dans lequel je met les images et autres médias, et un dossier JS dans lequel je met tout le code JS, la page HTML est à la racine.
L'habillage
Là il est question de se servir du CSS pour habiller la page en dehors du jeu, dans mon cas j'ajoute juste une bordure à la balise canvas et je n'y affiche pas la souris :
canvas { border: 1px solid black; cursor: none; }
Les images
Plutôt que de dessiner les graphismes de mon jeu avec uniquement du code, je vais me servir de quelques images pour le terrain, la balle et les raquettes. Elles sont placées dans le dossier ASSETS et il me suffira de les charger au moment voulu dans mon programme.
Le code Javascript
Hé bien voilà, c'est tout pour la mise en place, pas bien compliqué, tout le reste ce n'est que du code Javascript, libre à vous d'habiller la page HTML comme vous le sentez, tout ce qui nous intéresse c'est la balise canvas, la zone de dessin. Pour le code, j'ai choisi de partir sur du Javascript pur et dur, autant commencer par la base, exit donc Jquery et consorts. On commence par vous donner tout le code d'un coup, histoire que vous ayez une vue d'ensemble ?
// charger les images du jeu var balle = new Image(); var joueur = new Image(); var ordi = new Image(); var fond = new Image(); balle.src = "assets/balle.jpg"; fond.src = "assets/fond.jpg"; ordi.src = "assets/raquette.jpg"; joueur.src = "assets/raquette.jpg"; window.onload = function() { // récupère le canva et son contexte var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); // variables var W = 512; var H = 256; var p1 = {}; var p2 = {}; var b = {}; var mouseX; var mouseY; init(); // initialisation du jeu function init() { canvas.width = W; canvas.height = H; b.w = balle.width; b.h = balle.height; b.x = W/2-5; b.y = H/2-5; b.vX = (parseInt(Math.random()*2)-1|1)*(Math.random()*6+2); b.vY = (parseInt(Math.random()*2)-1|1)*(Math.random()*6+2); p2.w = ordi.width; p2.h = ordi.height; p2.x = W-25; p2.y = (H-p2.h)/2; p2.score = 0; p1.w = joueur.width; p1.h = joueur.height; p1.x = 15; p1.y = (H-p1.h)/2; p1.score = 0; mouseY = p1.y; canvas.addEventListener("mousemove", souris, false); setInterval(main, 15); } // boucle principale function main(){ // ordinateur if (b.y<p2.y) p2.y -= 5; if (b.y>p2.y) p2.y += 5; // joueur p1.y = mouseY; // limite des objets limites(p1); limites(p2); // balle with (b) { x += vX; y += vY; if (y<10) y=10, vY*=-1; if (y>246-h) y=246-h, vY*=-1; if (x<0) initBalle(), p2.score++; if (x>492) initBalle(), p1.score++; if(collisions(b,p1)) { x = p1.x+p1.w+10; vX *= -1; vY = -Math.round((p1.y+p1.h/2)-(y+h/2)*.2)%8; } if(collisions(b,p2)) { x = p2.x-b.w-10; vX *= -1; vY = -Math.round((p2.y+p2.h/2)-(y+h/2)*.2)%8; } } // dessin final render(); } function initBalle(){ b.x = W/2-5; b.y = H/2-5; b.vY = (parseInt(Math.random()*2)-1|1)*(Math.random()*6+2); } // limites des raquettes function limites(ob){ if (ob.y<10) ob.y = 10; if (ob.y>256-ob.h) ob.y = 256-ob.h; } // collisions function collisions(A,B) { if (A.y+A.h < B.y || A.y > B.y+B.h || A.x > B.x+B.w || A.x+A.w < B.x) return false; return true; } // Dessine le jeu function render() { ctx.drawImage(fond,0,0); ctx.drawImage(balle, b.x, b.y); ctx.drawImage(ordi, p2.x, p2.y); ctx.drawImage(joueur, p1.x, p1.y); draw_score(); } function souris(e){ if (e.x != undefined && e.y != undefined){ mouseX = e.x; mouseY = e.y; } else { // Firefox patch mouseX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; mouseY = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; } } // Affiche le score function draw_score() { ctx.fillStyle = "white"; ctx.font = "24px Arial"; ctx.textAlign = "right"; ctx.fillText(p1.score + " ", W/2, 30); ctx.textAlign = "left"; ctx.fillText(" " + p2.score, W/2, 30); } }
Pour ceux qui ont déjà suivit la version Actionscript de ce même exercice, ça va pas trop dépaysés ?…. Bah oui, c'est pareil ! On retire tout le typage de l'AS, on ajoute deux trois méthodes propres à JS et le tour est joué. Et c'est là que ça devrait faire Tilt ! Oui en fait on s'en moque un peu du langage quand on crée un jeu, ce qui compte ce sont les algorithmes, les calculs et la logique, pour le reste ce n'est qu'une question de traduction, et ça c'est un super bon point pour tous ceux qui se désespéraient d'avoir appris tant de choses en AS3 et de devoir changer de techno, ce qui change c'est la manière dont on l'écrit, pas la manière dont c'est pensé
Etude du programme
Bon c'est pas tout ça mais on va quand même s'étudier un peu le code de plus près.
// charger les images du jeu var balle = new Image(); var joueur = new Image(); var ordi = new Image(); var fond = new Image(); balle.src = "assets/balle.jpg"; fond.src = "assets/fond.jpg"; ordi.src = "assets/raquette.jpg"; joueur.src = "assets/raquette.jpg";
Dès que le fichier JS est chargé ces lignes sont lues, on commence par créer quatre variables “images”, la balle, le joueur, l'ordi et le fond. Avec les quatre lignes suivantes on charge nos images dans ces variables. C'est un peu brutal comme méthode, un loader propre aurait été mieux, mais il y a 6 ko à charger, ça ne devrait pas poser de problème au programme… Puis on entre dans le vif du sujet avec :
window.onload = function() { ... }
Tout se passe là dedans, et ça nous dit tout simplement qu'il faut déclencher le programme lorsque tous les objets du document sont dans le DOM (pour faire simple : lorsque toute la page HTML a été chargée). Voyons à présent le contenu de notre programme :
// récupère le canva et son contexte var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d');
On crée une variable “canvas” qui récupère la zone de dessin placée dans la page HTML et une variable “ctx” qui définit le contexte (ici 2D) qui nous fournira tous les outils pour dessiner dans la zone de dessin. En gros, canvas c'est la toile, et context c'est les pinceaux.
// variables var W = 512; var H = 256; var p1 = {}; var p2 = {}; var b = {}; var mouseX; var mouseY;
La largeur, la hauteur, les deux joueurs, la balle et la position de la souris sur les deux axes…
Notez que pour créer des objets (joueurs et balle), pour l'instant vides, il me suffit d'utiliser des accolades.
init();
Les variables sont créées, on lance l'initialisation du jeu.
// initialisation du jeu function init() { canvas.width = W; canvas.height = H; b.w = balle.width; b.h = balle.height; b.x = W/2-5; b.y = H/2-5; b.vX = (parseInt(Math.random()*2)-1|1)*(Math.random()*6+2); b.vY = (parseInt(Math.random()*2)-1|1)*(Math.random()*6+2); p2.w = ordi.width; p2.h = ordi.height; p2.x = W-25; p2.y = (H-p2.h)/2; p2.score = 0; p1.w = joueur.width; p1.h = joueur.height; p1.x = 15; p1.y = (H-p1.h)/2; p1.score = 0; mouseY = p1.y; canvas.addEventListener("mousemove", souris, false); setInterval(main, 15); }
Avant de lancer le jeu, on défini la taille de la zone où l'on veut dessiner; puis on défini nos trois objets, par exemple pour la balle (c'est pareil pour les joueurs) :
b.w = balle.width; b.h = balle.height; b.x = W/2-5; b.y = H/2-5; b.vX = (parseInt(Math.random()*2)-1|1)*(Math.random()*6+2); b.vY = (parseInt(Math.random()*2)-1|1)*(Math.random()*6+2);
Deux variables locales (à l'intérieur de l'objet “b”) pour la largeur et la hauteur (récupérée depuis l'image qu'on à préalablement chargée), deux autres pour sa position sur les deux axes, et deux pour sa vitesse. A propos de la vitesse justement, deux indices, parseInt transforme un nombre décimal en entier et Math.random() renvoie un nombre décimal aléatoire compris en 0 et 1. A partir de là vous devriez pouvoir faire le calcul facilement, la vitesse varie entre 2 et 8 aléatoirement sur les deux sens des axes. On répète l'opération pour les joueurs en remplaçant la vitesse par le score. On défini la position de départ de la souris (utile pour le joueur mais pas pour l'ordinateur).
Et il ne reste plus qu'à lancer la boucle principale.
canvas.addEventListener("mousemove", souris, false); setInterval(main, 15);
On place un écouteur d'événement sur la zone de dessin pour regarder si la souris bouge, ce qui aura pour action de déclencher la fonction “souris” que nous verrons un peu plus tard. Puis on utilise un timer, qui va boucler toutes les 15 millisecondes et déclencher la fonction “main”, la boucle principale du programme.
Puisque tout est prêt… go :
// boucle principale function main(){ // ordinateur if (b.y<p2.y) p2.y -= 5; if (b.y>p2.y) p2.y += 5; // joueur p1.y = mouseY; // limite des objets limites(p1); limites(p2); // balle with (b) { x += vX; y += vY; if (y<10) y=10, vY*=-1; if (y>246-h) y=246-h, vY*=-1; if (x<0) initBalle(), p2.score++; if (x>492) initBalle(), p1.score++; if(collisions(b,p1)) { x = p1.x+p1.w+10; vX *= -1; vY = -Math.round((p1.y+p1.h/2)-(y+h/2)*.2)%8; } if(collisions(b,p2)) { x = p2.x-b.w-10; vX *= -1; vY = -Math.round((p2.y+p2.h/2)-(y+h/2)*.2)%8; } } // dessin final render(); }
Cette fonction, déclenchée toutes les 15 millisecondes, va gérer tous les objets de notre programme, et on commence par l'ordinateur et son intelligence artificielle (très artificielle même…) :
// ordinateur if (b.y<p2.y) p2.y -= 5; if (b.y>p2.y) p2.y += 5;
Traduction, si la balle est au dessus de la raquette de l'ordi, ce dernier monte à une vitesse de 5 pixels, si elle est en dessous, il descend. Donc, plus l'ordinateur est rapide et plus il est dur à battre, voire impossible si il va aussi vite ou plus que la balle.
// joueur p1.y = mouseY;
La raquette du joueur se place à la position de la souris sur l'axe Y.
// limite des objets limites(p1); limites(p2);
On va restreindre le déplacement des raquettes aux limites du terrain de jeu, pour cela on utilise une fonction “limites” que l'on verra un peu plus tard pour ne pas perdre le fil.
// balle with (b) { x += vX; y += vY; if (y<10) y=10, vY*=-1; if (y>246-h) y=246-h, vY*=-1; if (x<0) initBalle(), p2.score++; if (x>492) initBalle(), p1.score++; if(collisions(b,p1)) { x = p1.x+p1.w+10; vX *= -1; vY = -Math.round((p1.y-y)*.5); } if(collisions(b,p2)) { x = p2.x-b.w-10; vX *= -1; vY = -Math.round((p2.y-y)*.5); } }
Allez, c'est le gros morceau…
Il me reste à déplacer la balle, celle-ci a sa propre vitesse et réagit à son environnement en rebondissant sur certains murs ou sur les raquettes, parfois en prenant de l'effet. Je vais à présent travailler un même objet, la balle, je vais donc utiliser une petite instruction bien utile (avant d'attaquer la POO), qui est : “with”, traduction : “avec”…
En gros je dis à mon programme :
"avec la balle {
// fais des trucs
}"
Ici ça va me servir à simplifier un peu mon code, au lieu d'écrire par exemple “b.x” pour trouver la position de la balle, il me suffit d'écrire “x”. Le programme regarde d'abord si il trouve une variable (…, propriété ou méthode) à l'intérieur de l'objet avec (with) lequel on travaille, si il ne la trouve pas il regardera au niveau supérieur.
Et donc elle fait quoi la balle ?
x += vX; y += vY; if (y<10) y=10, vY*=-1; if (y>246-h) y=246-h, vY*=-1; if (x<0) initBalle(), p2.score++; if (x>492) initBalle(), p1.score++
Elle se déplace sur chaque axe a sa propre vitesse.
Si elle touche le sol ou le plafond, elle rebondit, on inverse la vitesse surl'axe.
Si elle dépasse un des joueurs, on la réinitialise (initBalle) et on incrémente le score du joueur adverse.
Reste à gérer les collisions avec les raquettes :
if(collisions(b,p1)) { x = p1.x+p1.w+10; vX *= -1; vY = -Math.round((p1.y+p1.h/2)-(y+h/2)*.2)%8; } if(collisions(b,p2)) { x = p2.x-b.w-10; vX *= -1; vY = -Math.round((p2.y+p2.h/2)-(y+h/2)*.2)%8; }
On va tester si il deux objets se touchent à l'aide d'une fonction “collisions” que nous verrons juste après, notez simplement qu'on lui passe deux objets en paramètres, la balle et un des joueurs. Si la collision à bien lieu, on replace la balle sur la raquette (évite que la balle se retrouve prisonnière de la raquette si sa vitesse est trop faible), on inverse le sens sur l'axe X et on fait varier la vitesse sur l'axe Y en fonction de la distance de la balle par rapport à la raquette lors du contact.
Tout les calculs sont prêts, nos objets bien positionnés, il reste à dessiner tout ça :
// dessin final render();
on va voir la fonction render un peu plus loin mais avant revenons sur quelques autres petites fonctions qui nous ont été utiles précédemment.
function initBalle(){ b.x = W/2-5; b.y = H/2-5; b.vY = (parseInt(Math.random()*2)-1|1)*(Math.random()*6+2); }
Avec ce bout de code on réinitialise la balle à chaque fois que c'est nécessaire, on la replace au centre du jeu, et on retire la vitesse sur l'axe Y, on ne touche pas l'axe X puisque c'est au joueur qui a gagné de servir.
La limite de position des raquettes :
// limites des raquettes function limites(ob){ if (ob.y<10) ob.y = 10; if (ob.y>256-ob.h) ob.y = 256-ob.h; }
On restreint tout simplement l'objet passé en paramètre de la fonction à une position minimale et maximale sur l'axe des Y.
Passons aux collisions :
// collisions function collisions(A,B) { if (A.y+A.h < B.y || A.y > B.y+B.h || A.x > B.x+B.w || A.x+A.w < B.x) return false; return true; }
On vérifie si deux objets A et B sont ou non en contact et on renvoie le résultat.
Pour les détails du calcul, jetez un oeil à cette fiche ( http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/fiche_collisions ), c'est en Actionscript mais c'est pareil.
Bon, on y est presque, mais avant de dessiner notre jeu il nous manque encore un élément essentiel : déplacer le joueur.
Rappelez-vous, au début de l'exercice on à créé un écouteur d'événement sur le canvas pour écouter les mouvements de la souris et déclencher la fonction suivante :
function souris(e){ if (e.x != undefined && e.y != undefined){ mouseX = e.x; mouseY = e.y; } else { // Firefox patch mouseX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; mouseY = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; } }
L'écouteur appelle cette fonction à chaque fois que la souris bouge, et il lui passe l'objet de l'événement (“e”, la souris) en paramètre. Là il y a une petite astuce, un patch à mettre pour Firefox qui ne récupère pas les coordonnées de la souris de la même manière que les autres.
Et nous y voilà, il nous reste deux choses à faire, dessiner le jeu et le score.
// Dessine le jeu function render() { ctx.drawImage(fond,0,0); ctx.drawImage(balle, b.x, b.y); ctx.drawImage(ordi, p2.x, p2.y); ctx.drawImage(joueur, p1.x, p1.y); draw_score(); }
C'est très simple, on va utiliser les méthodes proposés par le contexte, ici “drawImage” pour dessiner chaque image à la bonne position. On commence par le terrain, puis la balle, les deux joueurs et enfin le score.
Pour le score on va faire une seconde fonction :
// Affiche le score function draw_score() { ctx.fillStyle = "white"; ctx.font = "24px Arial"; ctx.textAlign = "right"; ctx.fillText(p1.score + " ", W/2, 30); ctx.textAlign = "left"; ctx.fillText(" " + p2.score, W/2, 30); }
Cette fois on utilise les outils de texte, on défini le style de la police à utiliser, sa couleur, son alignement, son texte et sa position. Ici on a besoin d'afficher le score des deux joueurs.
Conclusion
PONG est un jeu très simple au programme vraiment accessible, si vous ne l'avez pas déjà fait, je vous recommande d'aller jeter un oeil à la version Flash de ce tutorial, vous y apprendrez d'autres astuces qui marcherons aussi avec Javascript. Avec ce premier exercice très simple, mon but était de vous familiariser un peu avec Javascript et permettre à ceux qui viennent de l'AS de faire le pont. Par la suite je vais commencer à utiliser des librairies et des frameworks qui vont nous permettre d'aller encore plus près que ce que nous permettait AS3, mais chaque chose en son temps et j'espère que cette petite mise en bouche vous aura été agréable.
Les sources
