Exercice 11 - TETRIS
Bonjour,
Nous voici arrivés à la fin de la première série d'exercices.
Il est temps de vous mettre à l'épreuve pour valider vos acquis.
Au travers des 10 précédents exercices nous avons décortiqué toutes les notions essentielles à connaitre pour commencer à concevoir des jeux vidéo.
Avant d'aller plus loin et de passer à la deuxième série, je vous propose un ultime travail, cette fois nous n'allons pas détailler ensemble chaque partie du code.
Ca va être à vous de prendre tout ça en main, mais rassurez-vous, la totalité du code a été commenté ligne par ligne, profitez-en ça sera rarement le cas…
Tetris n'est pas un jeu simple, mais aucune notion ne vous est inconnue si vous avez fait les exercices précédents.
Tout d'abord le résultat :
*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 TETRIS c'est quoi ? (merci Wikipedia)
Tetris est un jeu vidéo de puzzle conçu en 1984 par Alekseï Pajitnov. Bâti sur des règles simples et exigeant intelligence et adresse, il est l'un des jeux vidéo les plus populaires au monde. Ses versions sont innombrables, y compris en 3D, et cette multiplicité se décline sur tous les types d'ordinateurs.
Il est considéré comme un des grands classiques de l'histoire des jeux vidéo aux côtés de Pong, Space Invaders ou encore Pac-Man.
Des pièces de couleur (parfois il n'y a pas de couleur, et ce sont les motifs sur les pièces qui changent) et de formes différentes descendent du haut de l'écran. Le joueur ne peut pas ralentir ou empêcher cette chute mais peut l'accélérer ou décider à quel angle de rotation (0°, 90°, 180°, 270°) et à quel emplacement latéral l'objet peut atterrir. Lorsqu'une ligne horizontale est complétée sans vide, elle disparaît et les blocs supérieurs tombent. Si le joueur ne parvient pas à faire disparaître les lignes assez vite et que l'écran se remplit jusqu'en haut, il est submergé et la partie est finie.
Le jeu ne se termine donc jamais par la victoire du joueur. Avant de perdre, le joueur doit tenter de compléter un maximum de lignes. Faire une seule ligne ne rapporte que 40 points, alors qu'en faire 2 en rapporte 100, 3 lignes rapportent 300 et 4 lignes (le maximum) en rapportent 1200. Le nombre de points est augmenté à chaque niveau selon l'équation f(p, n)= p(n+1) où p est le nombre de points au niveau 0 et n le niveau.
Les pièces de Tetris, sur lesquelles repose entièrement le jeu, sont des tétrominos. Il en existe sept formes différentes, toutes basées sur un assemblage de quatre carrés – le mot « Tetris » (du préfixe grec tetra-, qui signifie quatre) prend donc tout son sens. Le joueur peut faire tourner plusieurs fois, à gauche et/ou à droite selon la version, de 90° n'importe quel bloc pour le poser de la façon désirée pendant que le bloc descend. Chacune des sept pièces dispose d'une couleur qui lui est propre, et certains joueurs se réfèrent aux pièces seulement par ce détail. Au désarroi de ceux-ci, la couleur des pièces varie généralement d'une version de Tetris à une autre. Cependant, d'après les consignes de la Tetris Company, ceci n'a aucune incidence sur le jeu.
Le champ de jeu, aussi connu sous l'appellation « puits » dans les anciens Tetris et en tant que « matrice » dans les plus récents, est l'espace dans lequel tombent les pièces. Il dispose toujours d'une grille en arrière-plan, visible ou non, dont les cases sont de la même grandeur que les carrés des pièces, et que celles-ci suivent dans leur chute. Il est également entouré par une armature appelée « tétrion », infranchissable, qui pose les limites du champ de jeu.
La vitesse de la chute des pièces est déterminée par le niveau auquel vous êtes. Plus le niveau est élevé, plus les pièces tombent vite. Au niveau 0 on peut faire 5-6 déplacements latéraux avant que la pièce tombe d'un rang, au niveau 9 on ne peut plus faire que 1-2 mouvements latéraux. Le niveau 15 équivaut au niveau de la vitesse à tout le temps appuyer sur la flèche du bas, il n'y a plus de déplacements latéraux possibles directement. Le joueur peut alors dans ce cas tenter de les réaliser par des successions rapides de rotations, une pièce n'est définitivement posée sur l'écran de jeu que dans le cas où le joueur cesse toute action sur celle-ci. En effectuant continuellement des rotations sur une pièce, le joueur l'empêche donc de se poser définitivement, il est alors possible d'opérer des déplacements latéraux et selon les cas il peut être envisageable de faire passer une pièce par dessus une autre déjà posée. Ainsi il est toujours possible de placer et d'orienter de façon entièrement libre une pièce quel que soit le niveau de jeu en cours.
Nous n'allons bien entendu pas reproduire la totalité du comportement du jeu, d'une part car il s'agit d'un exercice et non d'un moteur tout prêt, et d'autre part car il faut bien qu'il vous reste un peu de grain à moudre de votre côté
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
Manipulation de tableaux en Javascript : http://www.commentcamarche.net/contents/587-javascript-les-tableaux
Toute la série 1 des exercices : http://forums.mediabox.fr/wiki/tutoriaux/javascript/divers
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>TETRIS</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>
L'habillage
J'ajoute une bordure à la balise canvas :
canvas { border: 1px solid black; }
Les images et les sons
Je prépare tous mes assets, c'est à dire toutes les images prédécoupéeset les sons qui vont servir dans mon jeu.
Je range le tout dans le dossier “assets”.
Le code Javascript
Le code est répati en quatre fichiers, le programme principal (“jeu.js”) et les classes des objets.
On commence par “jeu.js” :
// variables var canvas,ctx,W,H,posX,posY,images,grille,pieces,lateral,vitesse,timer,piece,nextP,nextColor,stock,preview,cadence,T,jeuX,jeuY,previewX,previewY,sounds; var fond = new Image(); fond.src = "assets/fond.jpg"; // importer un fichier function include(fileName){ document.write("<script type='text/javascript' src='js/"+fileName+".js'></script>" ); } include("piece"); // importer la classe Piece include("bloc"); // importer la classe Bloc include("sounds"); // importer la classe Sounds window.onload = function() { // Préparer le jeu canvas = document.getElementById('canvas'); // récupére le canvas ctx = canvas.getContext('2d'); // récupére le context W = 480; // largeur du jeu H = 480; // hauteur du jeu posX = canvas.offsetLeft; // décalage du canvas sur X posY = canvas.offsetTop; // décalage du canvas sur Y canvas.width = W; // largeur du canvas canvas.height = H; // hauteur du canvas T = 21; // largeur des tuiles jeuX = 4*T; // position de la zone de jeu sur x jeuY = 2*T; // position de la zone de jeu sur y previewX = 15*T; // position de la zone de preview sur x previewY = 2*T; // position de la zone de preview sur y canvas.setAttribute('tabindex','1'); // sélectionne le canvas dans la page canvas.focus(); // donne le focus au canvas loadImages(5); // charge 5 images } function loadImages(nbImg){ // Charger des images images = []; // vide le tableau contenant les images for(i=1; i<nbImg+1; i++){ // boucle sur le nombre d'images var b = new Image(); // crée une nouvelle image b.src = "assets/tuile"+i+".jpg"; // associe son bitmap b.onload = function() { // quand l'image est chargée images.push(this); // ajoute la au stock d'images if(--nbImg==0) init(); // s'il n'y a plus d'image à charger initialise le jeu }; } } function init() { // Initialer le jeu stock = []; // liste des blocs de la zone de jeu preview = []; // liste des blocs de la zone de preview grille = []; // grille affichée à l'écran pieces = []; // liste des différents types de pieces vitesse = 18; // vitesse du jeu timer = 0; // décompte le temps passé piece = null; // la piece en cours de jeu nextP = Math.floor(Math.random()*7); // référence de la prochaine piece nextColor = Math.floor(Math.random()*5); // référence de la prochaine couleur cadence = setInterval(update, 15); // cadence du jeu for(var i=0; i<20; i++) {grille.push([0,0,0,0,0,0,0,0,0,0])}; // création dela grille de jeu for(var i=0; i<7; i++) {pieces.push(new Piece(i,T,grille,stock))}; // création des types de pieces document.addEventListener("keydown", keyDown, false); // écoute l'appui d'une touche sur la page sounds = new Sounds(); } function keyDown(e){ // Gestion du clavier if (e.keyCode == 37 && piece.checkMove("g")) lateral=true; // déplace la piece à gauche if (e.keyCode == 39 && piece.checkMove("d")) lateral=true; // déplace la piece à droite if (e.keyCode == 38) piece.rotate(), sounds.rotate.play(); // oriente la piece if (e.keyCode == 40) while(piece.checkMove("b")) timer = 0; // fait checkLine la piece } function update(e) { // Mise à jour du jeu piece == null ? newPiece() : movePiece(); // crée une piece si aucune en cours, sinon déplace la lateral = false; // annule les mouvements latéraux checkLine(); // vérifie si une ligne est remplie render(); // rendu graphique du jeu }; function newPiece(){ // Créer une piece preview = []; // vide le tableau de preview piece = pieces[nextP]; // la piece devient la nouvelle piece piece.init(nextColor); // initialise la piece avec la bonne couleur nextP = Math.floor(Math.random()*7); // référence de la nouvelle piece nextColor = Math.floor(Math.random()*5); // référence de la nouvelle couleur pieces[nextP].buildPreview(preview,nextColor); // construit la preview de la nouvelle piece if(!piece.buildPiece()) gameover(); // construit la piece, fin de partie si un bloc touche le haut } function movePiece(){ // Déplacer la piece if (timer>=vitesse || lateral) { // si mouvement latéral ou temps écoulé if (piece.checkMove("b")) { // si la piece peut descendre elle descend lateral ? piece.Y-- : timer = 0; // si mouvement latéral la piece remonte sinon fin du temps } else { // sinon sounds.touchDown.play(); // bruitage piece.drawInGrid(); // ajoute les blocs de la piece à la grille piece = null; // vide la piece en cours for (var i=0;i<stock.length;i++){ // parcours tous les blocs var b = stock[i]; // référence le bloc testé if (b.T == "piece") { // si le bloc fait partie de la piece stock[i--] = new Bloc("tuile",b.couleur,b.x/T,b.y/T); // remplace le bloc par une tuile } } } } else { // sinon timer++; // incrémente le temps } } function checkLine(){ // Vérifier si une ligne est remplie var full,couleur,C,L,b,i, ligne = 19; // variables locales while (ligne>=0) { // teste toutes les lignes de l'aire de jeu à partir de la dernière full = true; // par défaut la ligne est remplie for(C=0; C<10; C++){ // vérifie chaque colonne if(!grille[ligne][C]) full = false; // si une case est vide la ligne n'est pas remplie } if (full) { // si la ligne est remplie sounds.full.play(); // bruitage for (i=stock.length-1;i>=0; i--){ // parcours tous les blocs if(stock[i].L == ligne) stock.splice(i,1); // retire tous les blocs concernant cette ligne } for (L=ligne; L>0; L--) { // remonte les lignes depuis la dernière for (C=0; C<10; C++) { // parcours toutes les colonnes de la ligne grille[L][C] = grille[L-1][C]; // la case prend la référence de celle du dessus if (grille[L][C]) { // si la case n'est pas vide for (i=0;i<stock.length;i++){ // parcours tous les blocs b = stock[i]; // référence le bloc en cours if(b.L == L-1 && b.C == C) { // si il s'agit du bloc au dessus de la case couleur = b.couleur; // récupére la couleur du bloc stock.splice(i,1); // supprime la le bloc } } stock.push(new Bloc("tuile",couleur,C,L)); // crée une nouvelle tuile pour cette case } } } for (C=0; C<10; C++) grille[0][C] = 0; // vide la première ligne de l'aire de jeu ligne++; // indécrement la ligne } ligne--; // décrémente la ligne } } function gameover(){ // Terminer la partie clearInterval(cadence); // stoppe la boucle principale alert("game over"); // signale au joueur qu'il a perdu init(); // relance le jeu } function render() { // Dessiner le jeu ctx.fillStyle = "rgb(51,51,51)"; ctx.fillRect(0, 0, W, H); ctx.drawImage(fond, 0, 0); for(var i=0; i<stock.length; i++){ ctx.drawImage(images[stock[i].couleur], stock[i].x+jeuX, stock[i].y+jeuY); } for(var i=0; i<preview.length; i++){ ctx.drawImage(images[preview[i].couleur], preview[i].x+previewX, preview[i].y+previewY); } }
J'utilise trois sortes d'objets dans le programme, les blocs qui composent les pieces, les pieces avec leur plans et les bruitages.
Voyons les blocs pour commencer avec “bloc.js” :
// l'objet Bloc function Bloc(type,color,C,L,cX=0,cY=0){ this.couleur = color; // couleur du bloc this.C = C; // colonne this.L = L; // ligne this.x = (C+cX)*T; // position sur x this.y = (L+cY)*T; // position sur y this.T = type; // type de bloc this.move = function(X,Y){ // Déplacer le bloc if (this.T == "piece") { // si le bloc fait partie de la piece en cours this.x = (this.C+Math.floor(X))*T; // nouvelle position sur x this.y = (this.L+Math.floor(Y))*T; // nouvelle position sur y } } }
Pour les sons ça se passe dans “sounds.js” :
// l'objet Piece function Sounds(){ this.full = new Audio('assets/SFX_line.ogg'); this.touchDown = new Audio('assets/SFX_PieceTouchDown.ogg'); this.rotate = new Audio('assets/SFX_rotate.ogg'); }
Et on attaque la grosse partie avec “piece.js” :
// l'objet Piece function Piece(mapNum, T, grid, stock){ this.X = 4; this.Y = 0; this.T = T; this.grille = grid; this.stock = stock; this.color = Math.floor(Math.random()*5); var C; var L; var temp; var maps = [ [[1,1,1,1],[0,0,0,0],[0,0,0,0],[0,0,0,0]], [[1,1,1,0],[0,0,1,0],[0,0,0,0],[0,0,0,0]], [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], [[1,1,0,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]], [[0,1,0,0],[1,1,0,0],[1,0,0,0],[0,0,0,0]] ] var map = maps[mapNum]; this.init = function(color){ // Initialiser la piece this.X = 4; // colonne this.Y = 0; // ligne this.color = color; // couleur } this.drawInGrid = function(){ // Ajouter la piece dans la grille for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la ligne if (map[L][C]) { // si la case est remplie this.grille[this.Y+L][C+this.X] = map[L][C]; // mise a jour de la grille } } } } this.buildPiece = function(){ // Construire la piece for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la piece if (map[L][C]) { // si la case est remplie this.stock.push(new Bloc("piece",this.color,C,L,this.X,this.Y)); // ajoute le bloc correspondant if (this.grille[this.Y+L][this.X+C]) return false; // si le bloc touche le haut de la zone de jeu, partie perdue } } } return true; // la piece a été construire } this.buildPreview = function(tab,color){ // Construire la preview for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la piece if (map[L][C]) { // si la case est remplie tab.push(new Bloc("preview",color,C,L)); // ajoute le bloc correspondant } } } } this.checkMove = function (dir) { // Vérifier si la piece peut bouger var col = 0; // la nouvelle colone var lig = 0; // la nouvelle ligne if(dir=="b") lig = 1; // si la piece descend if(dir=="g") col = -1; // si la piece va a gauche if(dir=="d") col = 1; // si la piece va a droite for (var L=0; L<4; L++) { // lignes de la piece for (var C=0; C<4; C++) { // colonnes de la piece if(map[L][C]) { // si la case n'est pas vide if(dir=="b" && this.Y+L+1>=20) return false; // si le bloc sort par le bas, ne bouge pas if(dir=="g" && this.X<=0) return false; // si le bloc sort par la gauche, ne bouge pas if(dir=="d" && this.X+C+1>9) return false; // si le bloc sort par le droite, ne bouge pas if(this.grille[this.Y+L+lig][this.X+C+col]) return false; // si le bloc est occupée dans la grille, ne bouge pas } } } this.X += col; // nouvelle colone this.Y += lig; // nouvelle ligne if(dir=="b") { // si la piece descend for (var i=0; i<this.stock.length; i++) { // parcours tous les blocs this.stock[i].move(this.X,this.Y); // déplace le bloc } } return true; // la piece peut bouger } this.rotate = function() { // Changer le sens de la piece if (this.checkRotation()){ // si la piece peut bouger for (var i=this.stock.length-1; i>=0;i--){ // parcours tous les blocs if (this.stock[i].T == "piece") this.stock.splice(i,1); // retire les blocs de la piece } this.buildPiece(); // construit la nouvelle piece } } this.checkRotation = function() { // Tester si la nouvelle orientation est possible temp = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; // orientation temporaire de la piece for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la ligne temp[C][3-L] = map[L][C]; // rotation de la piece } } while (!(temp[0][0] || temp[1][0] || temp[2][0] || temp[3][0])) { // Tant que la première colonne est vide for (L=0; L<4; L++) { // lignes de la piece for (C=1; C<4; C++) { // colonnes de la ligne temp[L][C-1] = temp[L][C]; // décale les colonnes vers la gauche } temp[L][3] = 0; // vide la dernière colonne } } for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la ligne if (temp[L][C]){ // si la case est remplie if (this.grille[L + this.Y][C + this.X] || C + this.X > 9 || L + this.Y > 19) return false; // si une erreur est détectée la piece n'est pas tournée } } } map = temp; // valide la nouvelle orientation de la piece return true; // la piece est tournée } }
Etude du programme
L'intégralité du code est commenté, ligne par ligne, vous avez donc toute la traduction
Ca va être à vous de jouer, il n'y a aucune notion que nous n'aillons vus dans les exercices précédents.
Si vous travaillez avec un éditeur du type Notepad++, je vous recommande de replier tous les blocs de niveau 1 (ou 2).
Vous obtiendrez ainsi l'affichage simplifié que nous allons parcourir rapidement ensemble pour voir la logique générale du programme.
On commence avec le programme principal :
// variables var canvas,ctx,W,H,posX,posY,images,grille,pieces,lateral,vitesse,timer,piece,nextP,nextColor,stock,preview,cadence,T,jeuX,jeuY,previewX,previewY,sounds; var fond = new Image(); fond.src = "assets/fond.jpg"; function include(fileName){ // importer un fichier include("piece"); // importer la classe Piece include("bloc"); // importer la classe Bloc include("sounds"); // importer la classe Sounds window.onload = function() { // Préparer le jeu function loadImages(nbImg){ // Charger des images function init() { // Initialer le jeu function keyDown(e){ // Gestion du clavier function update(e) { // Mise à jour du jeu function newPiece(){ // Créer une piece function movePiece(){ // Déplacer la piece function checkLine(){ // Vérifier si une ligne est remplie function gameover(){ // Terminer la partie function render() { // Dessiner le jeu
Déclaration des variables globales, création du background, import des classes utiles.
On prépare le jeu comme d'habitude, on charge les images et on lance l'initialisation.
Le programme principal s'occupe des grandes lignes, il donne des ordres aux pièces et aux blocs, mais ne s'occupe pas de la manière dont ces derniers son implémentés, seul le résultat compte pour lui. Il va donc gérer les événements clavier, mettre à jour les informations, créer les nouvelles pieces et leur demander de se déplacer, vérifier qu'une ligne de la grille est remplie et agir en conséquence, réagir si le joueur a perdu la partie et gérer l'affichage.
L'autre partie intéressante c'est les pieces avec “piece.js” :
// l'objet Piece function Piece(mapNum, T, grid, stock){ this.X = 4; this.Y = 0; this.T = T; this.grille = grid; this.stock = stock; this.color = Math.floor(Math.random()*5); var C; var L; var temp; var maps = [ [[1,1,1,1],[0,0,0,0],[0,0,0,0],[0,0,0,0]], [[1,1,1,0],[0,0,1,0],[0,0,0,0],[0,0,0,0]], [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], [[1,1,0,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]], [[0,1,0,0],[1,1,0,0],[1,0,0,0],[0,0,0,0]] ] var map = maps[mapNum]; this.init = function(color){ // Initialiser la piece this.drawInGrid = function(){ // Ajouter la piece dans la grille this.buildPiece = function(){ // Construire la piece this.buildPreview = function(tab,color){ // Construire la preview this.checkMove = function (dir) { // Vérifier si la piece peut bouger this.rotate = function() { // Changer le sens de la piece this.checkRotation = function() { // Tester si la nouvelle orientation est possible }
Les pieces sont au nombre de 7, toutes de forme différente.
Les maps représentent les 7 formes possibles, composées de blocs indépendants.
On ne peut jouer qu'une pièce à la fois, 7 pièces suffisent donc pour tout le jeu.
Lorsqu'une pièce est placée elle disparait tandis que sa map est transposée sur la grille.
Chaque piece est indépendante et comporte un certain nombre de méthodes pour la manipuler.
Vous avez la logique générale et les commentaires, à vous de travailler
Conclusion
C'est terminé pour la première série d'exercices.
Débarassés des bases nous allons à présent pouvoir nous attaquer à des jeux un peu plus complexes.
Les sources
