cours18 - sbalev/processing101 GitHub Wiki
Lavez, Rincez, Répétez.
--Inconnu--
Notre ami Chip, ainsi que vos créatures ont fait du chemin ! Grâce à Chip nous avons appris les bases du dessin de formes en Processing. De là, Chip a appris à réagir à la souris, à bouger de manière autonome en utilisant des variables, à changer de direction avec des conditionnelles, à augmenter ses capacités et son corps avec des boucles, à organiser son code avec des fonctions, à encapsuler ses données dans des objets et, finalement à se dupliquer par l'intermédiaire de tableaux. Quelle aventure !
Cependant, en ressortant de ce cours, il est peu probable que vous ayez uniquement envie de faire bouger des robots sur votre écran, fussent-elles bien programmées (mais si c'est le cas, fort bien !).
Ce qu'il nous faut faire désormais, consiste à faire une pause et considérer ce que nous avons appris et la façon dont cela peut nous aider à réaliser ce que nous souhaitons faire. Quelle est votre idée de la façon dont les variables, les conditionnelles, les boucles, les fonctions, les objets peuvent vous aider à mener à bien vos projets ?
Chacun des projets que nous avons réalisés jusqu'à présent démontraient l'utilisation et la réalisation d'un idée ou caractéristique unique. Cela nous a permis d'apprendre à les utiliser isolément et plus facilement.
Dans le monde réel cependant, les projets impliquent souvent plusieurs modules en interaction. Ce chapitre vise à vous montrer comment un projet complexe est créé à partir de plusieurs sous parties. Vous, le.la programm.eur.euse, allez commencer avec la vision d'un projet à réaliser, pour ensuite apprendre à la scinder en sous-parties afin de mener à bien cette vision facilement.
Nous allons commencer avec un projet basique. Nous essayerons de réaliser un jeu simple possédant de l'interactivité, plusieurs objets, et un but. Notre objectif n'est pas de devenir bons en conception de jeu, mais bons en conception de logiciels. Mais comment aller de l'idée du jeu vers le code du jeu ? Comment implantez-vous vos propres algorithmes pour réaliser vos idées ? Nous allons réaliser un exemple où un projet large est divisé en quatre mini-projets, auxquels nous nous attaquerons un par un individuellement, pour finir par les assembler en un projet plus important qui réalisera notre idée originale.
Nous allons continuer à mettre l'accent sur la programmation orientée objet et chaque sous partie sera développée à l'aide d'une classe. Vous verrez à la fin à quel point cette technique est intéressante lorsqu'il s'agira de réutiliser les mini-projets au sein d'un plus important.
Voici notre plan d'action :
- Idée -- commencer avec l'idée d'un projet à réaliser.
-
Parties ou Modules -- Scinder cette idée en parties faciles à gérer individuellement.
- Pseudo-code -- Pour chaque partie, créer l'algorithme nécessaire.
- Implantation -- Implanter cet algorithme dans un langage de programmation.
- Objets -- Prenez les données et les fonctionnalités impliquées par cet algorithme et assemblez les dans une classe.
- Intégration -- Prenez toutes les classes de l'étape 2 et intégrez les dans un algorithme plus large.
Mais avant de nous lancer dans ce projet ambitieux, permettez-nous de réviser un concept important, celui d'algorithme dont nous aurons besoin largement par la suite.
Un algorithme est une procédure ou formule pour résoudre un problème. En programmation, un algorithme est une séquence d'étapes requises pour accomplir une tâche. Chaque exemple que nous avons vu précédemment dans ce cours impliquait un algorithme.
Finalement, un algorithme n'est pas très éloigné d'une recette de cuisine :
- Pré-chauffez le four à 200°C.
- Placez quatre blancs de poulet dans un plat.
- Étalez de la moutarde au miel sur les blancs.
- Faites cuire pendant 30 minutes.
Voici un joli algorithme pour cuisiner du délicieux poulet à la moutarde. Bien entendu Processing ne nous aidera pas beaucoup pour cette tâche, mais voici le pseudo-code qui pourrait en résulter :
prechaufferFour(200);
mettreDansPlat(4, "blanc de poulet");
etaler("moutarde");
cuire(200, 30);
Un exemple qui utilise un algorithme pour résoudre un problème mathématique est bien moins appétissant, mais probablement plus utile pour la suite. Essayons de décrire un algorithme qui évalue la somme d'une séquence de nombres de 1 à N :
SUM(N) = 1 + 2 + 3 + ... + N
où N est n'importe quel nombre entier plus grand que zéro :
- Placer
SUM = 0
et un compteurI = 1
- Répéter les étapes suivantes tant que
I
est inférieur ou égal àN
.- Calculer
SUM + I
et sauver le résultat dansSUM
. - Incrémenter la valeur de
I
par 1.
- Calculer
- La solution est sauvée dans
SUM
.
Essayons maintenant de transcrire ce pseudo-code en code Processing :
int sum = 0;
int n = 10;
int i = 1;
while (i <= n) {
sum = sum + i;
i++;
}
println(sum);
Ainsi, traditionnellement, la programmation est vue comme le procédé consistant à (1) développer une idée, (2) créer un algorithme pour implanter cette idée, (3) écrire le code qui implante l'algorithme. C'est ce que nous venons d'accomplir à la fois dans l'exemple du poulet à la moutarde et de la somme des nombres.
Cependant certaines idées sont trop complexes pour être réalisées en un unique algorithme, et nous allons revoir ces étapes et dire que la programmation est vue comme le procédé consistant à (1) développer une idée, (2) scinder cette idée en parties faciles à gérer, (3) créer un algorithme pour chaque partie, (4) écrire le code pour chaque partie, (5) créer un algorithme qui travaille avec chacune de ces parties, (6) intégrer le code des sous-parties ensemble.
Bien entendu, au fil du développement de chacune des ces étapes nous aurons certainement des idées de modification. Mais cette façon de procéder est un bon guide pour arriver à un résultat sans (trop d') encombre.
Source xkcd
Ainsi nous devons commencer par l'idée du programme que nous souhaitons réaliser. Décrivons donc notre jeu simple sous la forme d'un paragraphe :
Le but de ce jeu sera d'attraper des gouttes de pluie avant qu'elles ne touchent le sol. Suivant une fréquence qui dépendra d'un niveau de difficulté, une nouvelle goutte tombera du haut de l'écran suivant une position horizontale aléatoire et avec une vitesse verticale aléatoire. Le joueur doit attraper les gouttes de pluie avec la souris avec pour objectif de ne pas laisser une seule goutte toucher le bas de l'écran.
Maintenant, regardons si nous pouvons reprendre cette idée et la découper en parties plus petites. Comment procéder ? Nous pouvons déjà nous intéresser aux éléments du jeu : les gouttes et l'attrapeur. Ensuite nous pouvons nous intéresser au comportement de ces éléments. Par exemple nous aurons besoin d'un mécanisme de temporisation permettant de faire tomber des gouttes avec une fréquence donnée. Nous devrons enfin pouvoir déterminer si une goutte a été attrapée ou non.
Organisons ces parties un peu plus formellement :
- Partie 1 -- Développer un programme avec un cercle contrôlé par la souris. Ce cercle serra l'attrapeur.
- Partie 2 -- Développer un programme qui teste si deux cercles sont en intersection. Cela permettra de savoir si l'attrapeur à pris une goutte au piège.
- Partie 3 -- Développer un programme qui exécute une fonction toutes les N secondes.
- Partie 4 -- Développer un programme avec des cercles qui "tombent" du haut de l'écran vers le bas. Ce seront les gouttes de pluie.
Les parties 1 à 3 peuvent être réalisées assez directement. Nous allons voir cependant que la partie 4 qui peut sembler simple devra à son tour être découpée en sous-parties plus simple pour la mener à bien. C'est une technique qui est souvent utile dans les projets complexes.
Nous avons réalisé les étapes 1 et 2 de notre plan d'action. Les sections suivantes réaliseront les étapes 2.i, 2.ii et 2.iii pour chacune des sous parties : créer un algorithme en pseudo-code, l'implanter, et finir par une version orientée objet. Si nous réalisons correctement notre travail, ces classes seront directement utilisables pour être copiées dans le projet de jeu directement pour réaliser l'étape 3.
C'est probablement la partie la plus facile à écrire. Savoir que notre pseudo-code s'écrit en deux lignes est une bonne indication que cette partie n'aura pas besoin d'être subdivisée plus avant.
Pseudo code Attrapeur
- Effacer le fond.
- Dessiner une ellipse à la position de la souris.
En code Processing cela donne :
Exemple 18.1. L'attrapeur
void setup() {
size(400, 400);
}
void draw() {
background(255);
stroke(0);
fill(175);
ellipse(mouseX, mouseY, 64, 64);
}
Voilà une bonne chose de faite, mais nous n'avons pas fini. Notre programme n'est pas orienté objet. Afin d'incorporer facilement ce code à notre programme final il sera effectivement plus intéressant de le transformer en objet "attrapeur". Notre pseudo-code pourra alors s'écrire :
Pseudo code Attrapeur, version objet
- Setup:
- initialiser un objet
Attrapeur
- initialiser un objet
- Draw:
- Effacer le fond.
- Placer l'attrapeur à la position de la souris.
- Afficher l'attrapeur.
Essayons de réécrire ce code en version orientée objet :
Exemple 18.2. L'attrapeur version orientée objet
Attrapeur attrapeur;
void setup() {
size(400, 400);
attrapeur = new Attrapeur(32);
}
void draw() {
background(255);
attrapeur.placer(mouseX, mouseY);
attrapeur.dessiner();
}
class Attrapeur {
float r; // Rayon
float x, y; // Position
Attrapeur(float r) {
this.r = r;
x = 0;
y = 0;
}
void placer(float x, float y) {
this.x = x;
this.y = y;
}
void dessiner() {
stroke(0);
fill(175);
ellipse(x, y, 2 * r, 2 * r);
}
}
Exercice 18.1. Prévoyez la possibilité d'avoir des attrapeurs d'une autre couleur qu'on peut passer en argument du constructeur.
Nous devons savoir quand une goutte de pluie intersecte notre attrapeur. La fonctionnalité "intersection" est la partie sur laquelle nous désirons nous focaliser. Nous allons commencer par une simple balle rebondissante et essayer de déterminer quand deux balles rebondissantes sont en intersection. Pendant notre processus d'intégration cette fonction d'intersection sera incorporée dans la classe Attrapeur
pour attraper les gouttes.
Voici notre algorithme pour la partie "intersection" :
Pseudo code intersection
- Setup
- Créer deux balles
- Draw
- Si la balle 1 intersecte la balle 2, changer la couleur des deux en blanc, sinon rester en gris.
- Afficher les balles.
- Bouger les balles
Bien entendu la difficulté ici consiste à calculer l'intersection. Nous allons y venir, mais avant nous avons besoin d'une classe simple de balle rebondissante :
- Données
- Le rayon de la balle
- Les coordonnées X et Y
- La vitesse en X et en Y
- Fonctions
- Constructeur
- Régler le rayon à partir d'un argument
- Prendre une position aléatoire
- Prendre une vitesse aléatoire
- Bouger
- Incrémenter X par la vitesse en X
- Incrémenter Y par la vitesse en Y
- Si la balle rencontre l'un des bords, inverser la direction.
- Affichage
- Dessiner un cercle en X et Y
- Constructeur
Voici le code de la classe et un croquis avec deux balles. Dans le croquis final, nous aurons besoin d'un tableau pour un grand nombre de gouttes, mais pour le moment deux variables pour les balles suffiront.
Exemple 18.3. Un programme à deux balles
Balle b1, b2;
void setup() {
size(400, 400);
b1 = new Balle(64);
b2 = new Balle(32);
}
void draw() {
background(0);
b1.dessiner();
b2.dessiner();
b1.bouger();
b2.bouger();
}
class Balle {
float r; // Rayon
float x, y; // Position
float vx, vy; // Vitesse
Balle(float r) {
this.r = r;
x = random(width);
y = random(height);
vx = random(-5, 5);
vy = random(-5, 5);
}
void bouger() {
x += vx;
y += vy;
if (x < 0 || x > width) {
vx *= -1;
}
if (y < 0 || y > height) {
vy *= -1;
}
}
void dessiner() {
stroke(255);
fill(100, 50);
ellipse(x, y, 2 * r, 2 * r);
}
}
Nous sommes prêts à écrire l'algorithme d'intersection. Nous savons déjà qu'en Processing il est facile de déterminer la distance entre deux points avec la fonction dist()
. De plus nous connaissons déjà le rayon des deux balles (la variable r
au sein de chaque objet). La figure suivante nous montre comment comparer la distance entre les centres des deux balles (leurs positions) et la somme de leurs rayons pour déterminer si elles s'intersectent :
Nous pouvons en déterminer que, si (x1, y1) est la position d'une balle et r1 son rayon, de même avec (x2, y2) et r2 pour l'autre balle :
Si la distance entre (x1, y1) et (x2, y2) est inférieure à la somme de r1 et r2, alors le cercle 1 intersecte le cercle 2.
Notre travail désormais consiste à écrire une fonction qui retourne true
ou false
basée sur la question précédente.
Partie 2 Calcul d'intersection
Ajoutons cette fonction à notre croquis précédent :
boolean intersection(float x1, float y1, float r1, float x2, float y2, float r2) {
return dist(x1, y1, x2, y2) < r1 + r2;
}
et au début de draw()
nous pouvons désormais écrire :
if (intersection(b1.x, b1.y, b1.r, b2.x, b2.y, b2.r)) {
background(255, 0, 0);
} else {
background(0);
}
Cependant il nous reste une critique à formuler sur le code que nous venons d'écrire. Nous avons passé pas mal de temps à réaliser un programme orienté objet, et soudainement, nous avons une fonction en plus en dehors de tout objet. Une balle devrait être en mesure de dire si elle est en intersection avec une autre. Nous pouvons donc améliorer notre code pour ajouter une méthode dans Balle
qui fait cela : b1.intersecte(b2)
.
Ajoutez ce code dans la classe Balle
:
boolean intersecte(Balle b) {
return dist(x, y, b.x, b.y) < r + b.r;
}
Notez au passage que l'écriture s'en trouve simplifiée. Notez aussi l'utilisation à la fois des variables x
, y
et r
de la balle et des mêmes variables appartenant à l'autre balle passée en argument.
Dans l'onglet principal nous pouvons désormais retirer l'ancienne fonction intersection()
et changer le début de draw()
par :
if (b1.intersecte(b2)) {
background(255, 0, 0);
} else {
background(0);
}
Exemple 18.4. Intersection de deux balles
Balle b1, b2;
void setup() {
size(400, 400);
b1 = new Balle(64);
b2 = new Balle(32);
}
void draw() {
if (b1.intersecte(b2)) {
background(255, 0, 0);
} else {
background(0);
}
b1.dessiner();
b2.dessiner();
b1.bouger();
b2.bouger();
}
Le code complet de la classe Balle
est dans le dépôt principal.
Exercice 18.2. Au lieu de changer la couleur du fond lorsque les deux balles sont en intersection faites en sorte qu'elles changent de couleur. Vous aurez besoin d'ajouter un attribut de couleur à vos balles ainsi qu'une méthode permettant de leur indiquer de changer de couleur.
Exercice 18.3. Faites la même chose mais cette fois avec un tableau de 10 balles. Chaque balle doit changer de couleur si elle est en intersection avec une autre balle.
Exercice 18.4. En s'inspirant des balles, implantez l'intersection de 2 rectangles. Faites une classe Boite
avec les mêmes fonctionnalités que la classe Balle
. Faites un croquis avec deux boîtes qui bougent et qui changent de couleur lorsqu'elles se chevauchent.
Indication : il est plus facile de déterminer si deux rectangles ne s'intersectent pas.
Exercice 18.5. Pouvez-vous détecter l'intersection entre un cercle et un rectangle ? Ajoutez des méthodes correspondantes dans les classes Balle
et Boite
et faites un croquis avec une balle et une boîte qui changent de couleur lorsqu'elles se chevauchent.
Notre prochaine tâche consiste à développer un chronomètre qui exécute une fonction à intervalle donné. Nous allons à nouveau procéder en développant une petite esquisse dans Processing, puis nous la transformerons en objet (avec l'habitude vous n'aurez plus besoin de cette première étape).
Processing nous fournit des fonctions hour()
, minute()
, day()
, month()
et year()
pour gérer le temps. La fonction second()
existe aussi et pourrais nous sembler idéale puisque nous voulons compter le temps en secondes. Cependant, elle revient à zéro toutes les minutes, ce qui n'est pas pratique.
Mais une dernière fonction peut nous aider, elle s'appelle millis()
. Comme son nom le suggère, elle retourne des millisecondes, mais cette fois c'est le nombre de millisecondes écoulées depuis le lancement du croquis. Comme leur nom l'indique aussi, les millisecondes valent 1/1000ème de seconde (1s = 1000ms). Mais ce qui est particulièrement intéressant c'est que cette fonction ne se remet jamais à zéro. Ainsi demander le nombre de millisecondes écoulées à un moment puis à un autre et les soustraire nous donnera le nombre de millisecondes écoulées entre les deux mesures.
Par exemple, imaginons que nous désirions que le fond de l'écran change au rouge 5 secondes après le démarrage du croquis, nous pouvons écrire :
if(millis() > 5000) {
background(255, 0, 0);
}
Pour mieux comprendre encore comment cela fonctionne, compliquons le problème en essayant de faire changer la couleur de l'écran aléatoirement toutes les 5 secondes. Voici un pseudo-code :
- Setup:
- Sauvegarder le temps au démarrage (cela devrait être zéro, mais nous aurons besoin de la variable). Appelons cette variable
start
.
- Sauvegarder le temps au démarrage (cela devrait être zéro, mais nous aurons besoin de la variable). Appelons cette variable
- Draw:
- Calculer le temps passé actuellement (c-à-d.
millis()
moinsstart
) et le sauvegarder danstempsEcoule
. - Si le
tempsEcoule
est plus grand que 5000, effacer l'écran avec une nouvelle couleur aléatoire et remettrestart
au temps actuel. Cette étape redémarre le chronomètre.
- Calculer le temps passé actuellement (c-à-d.
Voici ce pseudo-code en code :
Exemple 18.5. Un chronomètre
int start;
int tempsTotal = 5000;
void setup() {
size(200, 200);
background(0);
start = millis();
}
void draw() {
float tempsEcoule = millis() - start;
if (tempsEcoule > tempsTotal) {
background(random(255));
start = millis();
}
}
La fonction millis()
n'ayant plus de secrets pour vous, nous allons pouvoir nous intéresser à la création d'une classe Chronometre
. Considérons les données requises pour un chronomètre, nous avons besoin d'un temps de départ (start
), et de la durée pendant laquelle il doit compter (tempsTotal
).
- Données
start
tempsTotal
- Fonctions
-
lancer()
pour démarrer le chronomètre -
estTermine()
pour savoir si la mesure est terminée.
-
Voici le code orienté objet que nous pourrions créer :
Exemple 18.6. Un chronomètre orienté-objet
Chronometre chrono;
void setup() {
size(200, 200);
background(0);
chrono = new Chronometre(5000);
chrono.lancer();
}
void draw() {
if (chrono.estTermine()) {
background(random(255));
chrono.lancer();
}
}
class Chronometre {
int start;
int tempsTotal;
Chronometre(int tempsTotal) {
start = millis();
this.tempsTotal = tempsTotal;
}
void lancer() {
start = millis();
}
boolean estTermine() {
return millis() - start > tempsTotal;
}
}
Exercice 18.6. Combinez les classes Balle
et Chronometre
pour faire un croquis dans lequel une balle arrête de bouger pendant 3 secondes lorsque l'utilisateur clique quelque part avec la souris.
Nous voici enfin arrivés à la dernière partie. Nous avons un attrapeur, nous savons déterminer les intersections, et nous savons réaliser un chronomètre. La pièce finale de notre puzzle consiste à modéliser les gouttes de pluie. Nous voudrions à la fin obtenir un tableau de gouttes tombant du haut de l'écran vers le bas. Puisque cette étape consiste à réaliser un tableau d'objets qui se déplacent tous, il est probablement utile de découper cette partie en sous parties plus faciles à gérer :
- Partie 4.1. Une goutte unique se déplaçant.
- Partie 4.2. Un tableau de ces gouttes.
- Partie 4.3. Un nombre variable de gouttes (apparaissant une par une).
- Partie 4.4. Une apparence plus funky.
Commençons donc par modéliser une goutte unique. Nous savons désormais faire cela presque les yeux fermés :
float x, y;
void setup() {
size(400, 400);
x = width / 2;
y = 0;
}
void draw() {
background(255);
fill(50, 100, 150);
noStroke();
ellipse(x, y, 16, 16);
y++;
}
Mais vous le savez désormais le code que nous avons écrit ci-dessus ne sert qu'à tester le comportement et l'algorithme, voici une version orientée objet :
class Goutte {
float x, y; // Position
float v; // Vitesse
color c; // Couleur
float r; // Rayon
Goutte() {
r = 8; // Nos gouttes ont toutes la même taille pour l'instant.
x = random(width);
y = -4 * r; // on commence légèrement au dessus du haut de l'écran.
v = random(1, 5);
c = color(50, 100, 150);
}
void bouger() {
y += v;
}
boolean enBas() {
return y > height + 4 * r;
}
void dessiner() {
fill(c);
noStroke();
ellipse(x, y, 2 * r, 2 * r);
}
}
Exercice 18.7. Avant d'aller plus loin il nous faut tester cette classe. Complétez le code suivant afin de vérifier notre implantation :
Goutte goutte;
void setup() {
size(200, 200);
___
}
void draw() {
background(255);
goutte.___
___
}
Il nous faut désormais multiplier les gouttes ! Cela tombe bien, nous avons appris à nous servir des tableaux :
Exemple 18.7. Plusieurs gouttes
Goutte[] gouttes = new Goutte[50];
void setup() {
size(400, 400);
for (int i = 0; i < gouttes.length; i++) {
gouttes[i] = new Goutte();
}
}
void draw() {
background(255);
for (int i = 0; i < gouttes.length; i++) {
gouttes[i].dessiner();
gouttes[i].bouger();
}
}
Le problème bien entendu avec le code ci-dessus est que toutes les gouttes commencent à tomber en même temps, bien que leur vitesse soit aléatoire. Nous souhaitons, souvenez-vous, que les gouttes tombent une par une toute les N
secondes. Pour l'instant nous n'allons pas nous occuper du chronomètre et juste faire en sorte que les gouttes tombent à chaque exécution de draw()
Nous allons aussi agrandir le tableau pour créer plus de gouttes.
Pour que cela fonctionne, il va nous falloir une variable indiquant le nombre total de gouttes, totalGouttes
. La plupart de nos exemple de tableaux impliquaient jusqu'alors de parcourir l'intégralité du tableau. Désormais nous ne voulons parcourir qu'une portion de la liste complète des gouttes (indiquée par totalGouttes
). Essayons d'écrire cela en pseudo-code :
- Setup:
- Créer un tableau de gouttes de 1000 cases.
- Régler
totalGouttes
à 0.
- Draw:
- Créer un nouvelle goutte dans le tableau à la position
totalGouttes
. Puisque cette variable commence à zéro, la première goutte sera bien dans la première case du tableau. - Incrémenter
totalGouttes
de façon à utiliser la prochaine case vide du tableau la prochaine fois. - Si
totalGouttes
dépasser la taille du tableau, la remettre à zéro pour recommencer. - Faire dessiner et bouger les gouttes créées (jusqu'à
totalGouttes
).
- Créer un nouvelle goutte dans le tableau à la position
Transformons ce pseudo-code en code :
Exemple 18.8.
Goutte[] gouttes = new Goutte[1000];
int totalGouttes = 0;
void setup() {
size(400, 400);
background(0);
}
void draw() {
background(255);
gouttes[totalGouttes] = new Goutte();
totalGouttes++;
if (totalGouttes >= gouttes.length) {
totalGouttes = 0;
}
for (int i = 0; i < totalGouttes; i++) {
gouttes[i].bouger();
gouttes[i].dessiner();
}
}
Nous nous sommes concentrés jusqu'à présent sur le comportement des gouttes de pluie. Cependant il serait intéressant de leur donner une apparence plus réaliste. La beauté de l'approche objet modulaire que nous avons suivi est que ce comportement que nous avons réalisé n'est plus à modifier, seule la partie dessin doit être changée.
Une façon (parmi tant d'autres) de dessiner nos gouttes peut consiste à afficher une séquence de cercles verticalement en commençant par de petits cercles et en les élargissant progressivement vers le bas.
background(255);
noStroke();
fill(0);
for (int i = 2; i < 8; i++) {
ellipse(width / 2, height / 2 + 4 * i, 2 * i, 2 * i);
}
Exercice 18.8. Nous pouvons aisément incorporer ce petit algorithme à notre classe Goutte
, il suffit pour cela de prendre en compte les attributs x
, y
et r
stockées dans cette dernière.
Nous y voilà ! Nous avons tous les éléments en main. Maintenant que nous avons développé toutes les pièces et vérifié qu'elles fonctionnaient, il ne reste plus qu'à les assembler proprement.
La première étape consiste à créer un croquis qui possède quatre onglets, un pour chacune des classes de notre programme :
-
GouttesDePluie
pour l'intégration, -
Attrapeur
pour le joueur, -
Goutte
, et -
Chronomètre
.
Il suffit de copier-coller le code des classes Attrapeur
, Goutte
et Chronometre
que nous avons déjà réalisé. Ces classes étant modulaires nous pourrons les ré-utiliser directement. Il nous reste en revanche à écrire l'onglet principal pour réaliser le jeu. Voici le pseudo-code de l'algorithme du jeu :
- Setup
- Créer un objet
Attrapeur
, - Créer un tableau de
Goutte
, - Placer
totalGouttes
à zéro, - Créer un objet
Chronomètre
, - Lancer le chronomètre.
- Créer un objet
- Draw
- Placer l'attrapeur à la position de la souris,
- Dessiner l'attrapeur,
- Bouger toutes les gouttes actives,
- Afficher toutes les gouttes actives,
- Si l'attrapeur intersecte une goutte :
- Retirer cette goutte de l'écran,
- Si le chronomètre est écoulé:
- Augmenter le nombre de gouttes,
- Relancer le chronomètre.
Notez que toutes les étapes de ce pseudo-code ont déjà été abordées, sauf l'étape consistant à retirer des gouttes de l'écran. Cela arrive dans tous les projets, il est souvent difficile de tout prévoir. Mais ce n'est pas très grave et nous allons y remédier durant l'assemblage des pièces.
Une façon de commencer consiste à ne pas s'occuper de la façon dont les parties interagissent mais à simplement reprendre le code des croquis précédent :
Attrapeur attrapeur;
Chronometre chrono;
Goutte[] gouttes = new Goutte[1000];
int totalGouttes = 0;
void setup() {
size(800, 800);
attrapeur = new Attrapeur(32);
chrono = new Chronometre(2000);
chrono.lancer();
}
void draw() {
background(255);
// De la partie 1
attrapeur.placer(mouseX, mouseY);
attrapeur.dessiner();
// De la partie 3
if (chrono.estTermine()) {
println("2 secondes se sont écoulées");
chrono.lancer();
}
// De la partie 4
gouttes[totalGouttes] = new Goutte();
totalGouttes++;
if (totalGouttes >= gouttes.length) {
totalGouttes = 0;
}
for (int i = 0; i < totalGouttes; i++) {
gouttes[i].dessiner();
gouttes[i].bouger();
}
}
L'étape suivante va consister à considérer le concepts développés précédemment et les faire fonctionner de concert. Par exemple, nous ne devrions créer de nouvelles gouttes que si le chronomètre est terminé :
if (chrono.estTermine()) {
gouttes[totalGouttes] = new Goutte();
totalGouttes++;
if (totalGouttes >= gouttes.length) {
totalGouttes = 0;
}
chrono.lancer();
}
Nous devons aussi ajouter un mécanisme de détection d'intersection. Nous allons le placer dans la classe Attrapeur
.
Exercice 18.9. Ajoutez une méthode boolean intersecte(Goutte goutte)
dans la classe Attrapeur
.
Nous pourrons ensuite utiliser cette méthode ainsi dans le code d'affichage et de déplacement des gouttes :
for (int i = 0; i < totalGouttes; i++) {
if (attrapeur.intersecte(gouttes[i])) {
gouttes[i].attrapee();
}
gouttes[i].dessiner();
gouttes[i].bouger();
}
Comment créer le code pour la goutte attrapée ? Nous pourrions la stopper et la dessiner hors de l'écran !
void attrapee() {
y = -1000;
v = 0;
}
Et... nous avons terminé !! Voici le code complet :
Exemple 18.9. Attrape-gouttes
Attrapeur attrapeur;
Chronometre chrono;
Goutte[] gouttes = new Goutte[1000];
int totalGouttes = 0;
void setup() {
size(800, 800);
attrapeur = new Attrapeur(32);
chrono = new Chronometre(2000);
chrono.lancer();
}
void draw() {
background(255);
attrapeur.placer(mouseX, mouseY);
attrapeur.dessiner();
if (chrono.estTermine()) {
gouttes[totalGouttes] = new Goutte();
totalGouttes++;
if (totalGouttes >= gouttes.length) {
totalGouttes = 0;
}
chrono.lancer();
}
for (int i = 0; i < totalGouttes; i++) {
if (attrapeur.intersecte(gouttes[i])) {
gouttes[i].attrapee();
}
gouttes[i].dessiner();
gouttes[i].bouger();
}
}
Le code complet des classes est dans le dépôt principal.
Exercice 18.10 Nous avons implanté un tableau de taille limitée mais variable en utilisant la variable totalGouttes
. Nous avons également « triché » légèrement en laissant les gouttes attrapées dans le tableau et en les cachant. Utilisez une ArrayList
à la place du tableau pour stocker les gouttes. Cela vous permettra d'ajouter les nouvelles gouttes plus facilement, ainsi que de retirer les gouttes attrapés ou ratées.
Exercice 18.11 Implantez un mécanisme de score pour le jeu. Le joueur commence avec 10 points. Pour chaque goutte qui atteint le bas de l'écran, le score est décrémenté de 1. Si 1000 gouttes sans que le score atteigne 0 un nouveau niveau commence avec des gouttes plus rapides. Si 10 gouttes atteignent le sol pendant un niveau, le joueur perd. Montrez le score sous la forme d'une pile de rectangles sur le côté de l'écran. Ne cherchez pas à implanter toutes ces caractéristiques en une fois !!
Le but de ce cours n'a pas été d'apprendre à faire un jeu, mais bien d'apprendre à s'organiser pour développer un projet avancé : développer une idée, la découper en sous partie que l'on sait gérer (ou redécouper ces sous-parties en sous-sous-parties si nécessaire), développer chaque algorithme en pseudo-code puis éventuellement le tester en code simple et enfin le rendre modulaire et réutilisable sous la forme d'une classe.
Il est important de se souvenir que s'habituer à cette technique prend du temps et de la pratique.
Dans les cours précédents, jusqu'à celui-ci nous nous sommes focalisés sur les fondamentaux de la programmation :
- Les données - sous la forme de variables ou de tableaux.
- Les structures de contrôle - sous la forme de conditionnelles et de boucles.
- L'organisation - sous la forme de fonctions et d'objets.
Ces concepts ne sont pas uniques à Processing et se retrouvent dans une majorité de langages de programmation. La syntaxe peut changer, mais les concepts resteront identiques.