Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Exercice 07 - PROXIMITY

Compatible JavaScript. Cliquer pour en savoir plus sur les compatibilités.Par Monsieur Spi, le 26 octobre 2015

Bonjour,

Je vous propose aujourd'hui un exercice basé sur un jeu très simple, ce qui va nous laisser le temps d'aborder quelques notions très utiles (POO) pour la conception de nos programmes.

Avant de commencer, voici à 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 PROXIMITY c'est quoi ? (et cette fois on ne dit pas merci Wikipedia)

Le jeu est composé d'une grille comprenant des pièces de deux types. Au départ du jeu le type des pièces est disposé aléatoirement dans la grille. Le joueur peut cliquer sur chaque pièces ce qui retourne à la fois la pièce sur laquelle il a cliqué mais également les 4 pièces adjacentes à la case (haut, bas, droite , gauche). La case haut gauche de la grille réagit cependant différemment puisqu'elle retourne également la case située en diagonale directement en bas à droite de la case.

Le joueur doit faire en sorte que toutes les pièces soient identiques dans la grille.

Un petit casse tête très simple à mettre en place mais difficile à résoudre.

Les pré-requis

Pour ce programme vous devez connaître :

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>PROXIMITY</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

Je prépare tous mes assets, c'est à dire toutes les images prédécoupées qui vont servir à mon jeu.

Je range le tout dans le dossier “assets”.

Le code Javascript

Cette fois notre code va être séparé en deux fichiers, le premier (“jeu.js”) décrit le comportement global de notre jeu :

// variables
var canvas, ctx, posX, posY, C, T, i, images, stock;
 
// importer un fichier 
function include(fileName){
	document.write("<script type='text/javascript' src='js/"+fileName+".js'></script>" );
}
include("tuile"); 
 
// quand la page est chargée
window.onload = function() {
    canvas = 		document.getElementById('canvas');
    ctx = 			canvas.getContext('2d');
	posX = 			canvas.offsetLeft;
	posY = 			canvas.offsetTop;
	canvas.width = 	480;
	canvas.height = 480;
	C = 			10;
	T = 			48;	
	loadImages(2);
}
 
// chargement des images
function loadImages(nbImg){
	images = [];
	for(i=1; i<nbImg+1; i++){
		var b = new Image();
		b.src = "assets/tuile"+i+".png";
		b.onload = function() {
			images.push(this);
			if(--nbImg==0) init();
		};
	}
}
 
// initialisation du jeu
function init() {
	stock = [];
	for (i=0;i<C*C;i++){stock.push(new Tuile(i,T,C,stock))};
	render();
	canvas.addEventListener("click", clic, false);
}
 
// cliquer sur une case    
function clic(e){
	if(stock[parseInt((e.clientX-posX)/T)+parseInt((e.clientY-posY)/T)*C].change(true)) finPartie();
	render();
}
 
// fin de partie
function finPartie(){
	alert("Fin de partie, cliquez pour rejouer.");
	init();
}
 
// Dessine le jeu
function render() {	
	for(i=0; i<stock.length; i++){
		ctx.drawImage(images[stock[i].frame-1], stock[i].x, stock[i].y);
	}
}

Le second fichier (“tuile.js”) va décrire un objet “tuile” que nous allons utiliser dans notre jeu :

 
// l'objet Tuile
function Tuile(index,taille,C,tab){
	this.x = parseInt(index%C)*taille;
	this.y = parseInt(index/C)*taille;
	this.X = parseInt(index%C);
	this.Y = parseInt(index/C);
	this.id = index;
	this.width = taille;
	this.height = taille;
	this.frame = Math.random()>.5 ? 1:2;
	this.change = function(selected=false){
		this.frame == 1 ? this.frame = 2 : this.frame = 1;
		if(selected==true){
			if (this.Y<C-1) 	tab[this.id+C].change();
			if (this.Y>0) 		tab[this.id-C].change();
			if (this.X<C-1) 	tab[this.id+1].change();
			if (this.X>0) 		tab[this.id-1].change();
			if (this.id == 0) 	tab[this.id+C+1].change();
		}
		for (i =0; i<tab.length; i++){
			if(tab[i].frame != this.frame) return false;
		}
		return true;
	}
}

Etude du programme

Comme vous pouvez le constater, il n'y a pas beaucoup de lignes de code, cependant nous avons à présent deux fichiers séparés pour construire notre jeu, commençons par le premier :

// variables
var canvas, ctx, posX, posY, C, T, i, images, stock;

Ce sont les variables globales (accessibles partout) de notre jeu.

// importer un fichier 
function include(fileName){
	document.write("<script type='text/javascript' src='js/"+fileName+".js'></script>" );
}
include("tuile");

Ca c'est une nouveauté par rapport aux exercices précédents.
J'ai besoin d'inclure un nouveau fichier Javascript au sein de mon application.
La fonction “include” qui est décrite ici se charge d'insérer au sein de la page HTML (le support) votre nouveau fichier.
Il suffit donc de lui indiquer que je veux charger le fichier “tuile”.

Pour ceux qui viennent de l'AS, ça ressemble fortement à un import.
Pour ceux qui n'ont jamais approché de près ou de loin les notions d'Objets et de POO, vous-vous demandez certainement pourquoi ne pas tout coder dans un seul et même fichier. Vous pourriez, mais ce serait moins propre et plus difficile à maintenir à la longue. On va donc profiter de ce petit jeu pour voir quelques notions intéressantes, par exemple ici comment charger dynamiquement des fichiers dans votre page, découper son programme et créer des objets.

// quand la page est chargée
window.onload = function() {	
	canvas = 		document.getElementById('canvas');	
	ctx = 			canvas.getContext('2d');
	posX = 			canvas.offsetLeft;
	posY = 			canvas.offsetTop;
	canvas.width = 		480;
	canvas.height = 	480;
	C = 			10;
	T = 			48;	
	loadImages(2);
}
 
// chargement des images
function loadImages(nbImg){
	images = [];
	for(i=1; i<nbImg+1; i++){
		var b = new Image();
		b.src = "assets/tuile"+i+".png";
		b.onload = function() {
			images.push(this);
			if(--nbImg==0) init();
		};
	}
}

Vous êtes à présent familiarisés avec ces deux blocs de programmation, on prépare le jeu, on charge les images et on lance l'initialisation lorsque tout est prêt. Si vous voulez plus de précisions regardez du côté du MEMORY ( http://forums.mediabox.fr/wiki/tutoriaux/javascript/divers/exercice_memory ).

// initialisation du jeu
function init() {
	stock = [];
	for (i=0;i<C*C;i++){stock.push(new Tuile(i,T,C,stock))};
	render();
	canvas.addEventListener("click", clic, false);
}

A l'initialisation, on vide le stock qui va contenir toutes les tuiles (cases) du jeu.
On boucle sur le nombre de cases total, pour chacune on crée une nouvelle tuile qu'on insére dans le stock.
On gére le rendu et on ajoute un écouteur d'événement souris au canvas (nous avons vus tout ça dans les exercices précédents).

Revenons une minute sur la création de nos tuiles.

new Tuile(i,T,C,stock)

Ici j'utilise le mot clé “new” suivit d'un appel à une Classe à laquelle je passe plusieurs paramètres.
Cette classe (ici “Tuile”) représente un moule duquel je peux créer autant d'objets de ce type que je le souhaite.
Le mot clé “new” permet de créer un nouvel objet et la classe détermine l'objet que je veux créer.
Notez qu'une classe s'écrit toujours avec une majuscule, c'est une convention.

C'est la première notion de POO (Programmation Orientée Objet), nous allons créer des “objets” à partir de moules (classes).
Il faut à présent décrire cette classe, ce moule, c'est ce que nous faisons dans le fichier “tuile.js”, mais avant de se lancer dans sa lecture, finissons la base de notre programme.

// cliquer sur une case    
function clic(e){
	if(stock[parseInt((e.clientX-posX)/T)+parseInt((e.clientY-posY)/T)*C].change(true)) finPartie();
	render();
}

Grâce à l'écouteur d'événement souris qu'on a placé sur le canvas, on peut récupérer la case sur laquelle le joueur vient de cliquer. Une fois de plus je vous recommande d'aller lire le TAQUIN ou le MEMORY pour les formules qui permettent de récupérer l'index d'une case dans une grille. Lorsqu'on a la bonne case, on fait appel à une méthode “change” qui lui est propre, si cette méthode renvoie “true” alors la partie est gagnée.

// fin de partie
function finPartie(){
	alert("Fin de partie, cliquez pour rejouer.");
	init();
}
 
// Dessine le jeu
function render() {	
	for(i=0; i<stock.length; i++){
		ctx.drawImage(images[stock[i].frame-1], stock[i].x, stock[i].y);
	}
}

Si la partie est gagnée on réinitialise le jeu.
On parcours le stock pour dessinner les tuiles.
Nous avons utilisé ces fonctions dans tous les jeux précédents, ce n'est donc pas une nouveauté ;)

Bien, pour le fonctionnement global du jeu c'est très simple, on peut le résumer ainsi :

Remplir la grille de tuiles.
Regarder sur quelle tuile le joueur clique.
Laisser la tuile faire son travail et nous dire si la partie est gagnée.
Gérer l'affichage du jeu.

Reste à comprendre comment sont faites les tuiles…

Notre objectif lorsqu'on programme en POO est de simplifier et de clarifier le programme.
Pour celà on va essayer de créer des objets les plus autonomes possibles.
Pour commencer on va créer un nouveau fichier pour notre objet afin de l'isoler du reste du programme.
Ce n'est pas obligatoire, vous pourriez tout coder dans un seul fichier, mais c'est plus propre car vous n'avez plus besoin de toucher au programme principal pour modifier vos objets. Vous n'êtes pas non plus obligés de créer un fichier par objet, vous pouvez regrouper certains objets dans un seul fichier.

Voyons de plus près ce que contient notre fichier “tuile.js” :

// l'objet Tuile
function Tuile(index,taille,C,tab){
	this.x = parseInt(index%C)*taille;
	this.y = parseInt(index/C)*taille;
	this.X = parseInt(index%C);
	this.Y = parseInt(index/C);
	this.id = index;
	this.width = taille;
	this.height = taille;
	this.frame = Math.random()>.5 ? 1:2;
	this.change = function(selected=false){
		this.frame == 1 ? this.frame = 2 : this.frame = 1;
		if(selected==true){
			if (this.Y<C-1) 	tab[this.id+C].change();
			if (this.Y>0) 		tab[this.id-C].change();
			if (this.X<C-1) 	tab[this.id+1].change();
			if (this.X>0) 		tab[this.id-1].change();
			if (this.id == 0) 	tab[this.id+C+1].change();
		}
		for (i =0; i<tab.length; i++){
			if(tab[i].frame != this.frame) return false;
		}
		return true;
	}
}

Il existe plusieurs manières de créer des objets en javascript, nous allons commencer par une méthode simple.

Tout d'abord un objet c'est quoi en Javascript ?

C'est une variable comme une autre, a la différence qu'elle contient plusieurs autres variables, fonctions ou objets qui lui sont propres.
Lorsque vous utilisez le mot clé “new” au sein de vos programmes, vous créez un objet, celui-ci peut être une fonction (qui est aussi une sorte de variable). Notre fonction “Tuile” est donc le moule de nos objets tuiles que nous utilisons dans le jeu, grâce au mot clé “new” je peut créer autant de tuiles que je le souhaite. On appelle ce type de fonction un “constructeur”, nous y reviendrons.

Vous remarquerez que je passe plusieurs paramètres à cette Classe, ils vont me servir à construire chaque objet.
La deuxième chose que vous remarquez immédiatement, c'est l'opérateur “this”.
Je vous recommande de lire la définition précise ici : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Op%C3%A9rateurs/L_op%C3%A9rateur_this
Pour la partie qui nous intéresse, “this” correspond à l'objet que nous sommes en train de créer, on indique donc que la variable ou la fonction qu'on décrit est propre à l'objet qu'on est en train de créer.

this.x = parseInt(index%C)*taille;
this.y = parseInt(index/C)*taille;
this.X = parseInt(index%C);
this.Y = parseInt(index/C);
this.id = index;
this.width = taille;
this.height = taille;
this.frame = Math.random()>.5 ? 1:2;

La position de l'objet dans le canvas (x et y).
Sa position dans la grille (X et Y).
Sa référence (l'index dans le stock).
Sa largeur et sa hauteur.
L'image qu'il doit afficher.

Ce sont les “propriétés” de notre objet, ce qui le caractérise, elles peuvent être différentes pour tout objet de même type que je crée. Pour faire simple, les humains (moules) ont des enfants (objets), ces enfants sont tous différents et pourtant tous de même type (humain).

this.change = function(selected=false){
	this.frame == 1 ? this.frame = 2 : this.frame = 1;
	if(selected==true){
		if (this.Y<C-1) 	tab[this.id+C].change();
		if (this.Y>0) 		tab[this.id-C].change();
		if (this.X<C-1) 	tab[this.id+1].change();
		if (this.X>0) 		tab[this.id-1].change();
		if (this.id == 0) 	tab[this.id+C+1].change();
	}
	for (i =0; i<tab.length; i++){
		if(tab[i].frame != this.frame) return false;
	}
	return true;
}

Maintenant que notre objet est décrit, voyons ce qu'il sait faire.

On va reprendre la parabole utilisée plus haut, les enfants ont une certaine autonomie, lorsque le parent indique à l'enfant qu'il doit aller se laver les dents, l'enfant s'exécute, mais il n'a pas besoin de l'intervention du parent pour effectuer cette action, il l'a déjà apprise et peut donc se débrouiller tout seul.

C'est exactement ce que nous faisons ici, nous apprenons à notre objet ce qu'il est en mesure de faire, et pour cela on utilise une fonction propre à l'objet (aussi appelée “méthode”). Ici il s'agit de la méthode “change”, elle va permettre de changer l'état de l'objet et de ses voisins dans la grille.

Voyons le détail :

this.change = function(selected=false){
	//...
}

Une nouveauté ici, on va ajouter un paramètre facultatif à notre méthode.
“selected” n'est pas obligatoire, si rien n'est passé à la méthode, “selected” aura par défaut la valeur “false”.

this.frame == 1 ? this.frame = 2 : this.frame = 1;

On change la frame à afficher pour l'objet en cours (celui sur lequel on viens de cliquer)

if(selected==true){
	//...
}

Le code ne sera déclenché que si on a passé “true” à la méthode.
Ceci permet de ne déclencher ce code que pour l'objet concerné, celui sur lequel on a cliqué.

if (this.Y<C-1) 	tab[this.id+C].change();
if (this.Y>0) 		tab[this.id-C].change();
if (this.X<C-1) 	tab[this.id+1].change();
if (this.X>0) 		tab[this.id-1].change();
if (this.id == 0) 	tab[this.id+C+1].change();

Si vous avez fait le Taquin (ça fait partie des pré-requis), ceci ne vous est pas étranger.

On cherche les cases voisines de celle sur laquelle on a cliqué. Mais comme notre tableau (le stock) n'a qu'une dimension (c'est une liste et pas une grille), on doit vérifier que la case voisine que l'on cherche à atteindre n'est pas en dehors de la grille.

Pour chaque voisine de la case cliquée, on change la frame à afficher, pour cela on utilise la méthode “change” de chaque objet, et pour éviter les traitements récursifs (ce sera l'objectif du prochain exercice) on utilise le paramètre “selected”.

Notez enfin que si la case sur laquelle on a cliqué est la toute première en haut a gauche, on modifie une case de plus, ceci permet de donner la possibilité de terminer le jeu en insérant un comportement légérement différent de toutes les autres cases.

for (i =0; i<tab.length; i++){
	if(tab[i].frame != this.frame) return false;
}
return true;

Souvenez-vous de notre code principal, il attend une réponse lorsqu'on clique sur une case, elle doit lui indiquer si la partie est gagnée ou non. On va donc faire une boucle sur toutes les tuiles du stock et vérifier si elles affichent toutes la même image, si ce n'est pas le cas la partie continue, sinon elle s'arrête.

Notez que ce n'est pas l'objet qui arrête la partie, mais le code principal, l'objet ne fait que renvoyer l'information au code principal.

Conclusion

Je me suis demandé si je devais vous proposer ce jeu si simple, en fait si on dépiaute un peu il n'y a rien de nouveau dans le principe du jeu. J'en ai donc profité pour vous donner quelques notions de POO en Javascript, mais comme vous allez le voir, il existe différentes manières de créer des objets.

Les sources