cours11 - sbalev/processing101 GitHub Wiki

Cours 11

Les fonctions

Jusqu'à présent, nos exemples et programmes en Processing ont toujours été courts. Nous n'avons quasiment jamais tapé de programme plus long qu'une centaine de lignes. Ce genre de programme serait l'équivalent d'un paragraphe au sein du cours, comparé à un cours complet.

En effet, l'intérêt de Processing réside en sa capacité à créer des croquis efficaces avec très peu de code. Mais plus nous allons continuer, plus nos projets seront complexes, communication réseau, traitement d'image, gestion des fichiers... Nous passerons de la centaine de lignes aux milliers ! En d'autres termes nous n'allons plus écrire des paragraphes, mais des essais, voir des livres ! Il serait difficile de tout placer directement dans setup() et draw().

Les fonctions sont un moyen d'extraire des parties de nos programmes et les séparer en pièces modulaires, rendant notre code plus facile à lire, ainsi qu'à comprendre et (ré-)utiliser. Considérons un instant le jeu Space Invaders. Les étapes de draw() pourraient être les suivantes :

  • Effacer le fond,
  • Dessiner le vaisseau spatial,
  • Dessiner les ennemis,
  • Déplacer le vaisseau en fonction des interaction clavier/souris,
  • Déplacer les ennemis.

Avant ce cours, nous aurions traduit ces étapes en code et placé ce dernier directement dans draw(). Les fonctions, cependant, nous permettent une autre approche du problème :

void draw() {
  background(0);
  drawSpaceShip();
  drawEnemies();
  moveShip();
  moveEnemies();
}

Le code ci-dessus nous montre que les fonctions nous rendront la vie plus simple avec du code plus clair et facile à gérer. Cependant, il nous reste à découvrir comment définir ces fonctions. Après tout, appeler des fonction est pour nous une habitude ! Nous faisons cela tout le temps quand nous écrivons line(), rect(), fill() etc. Mais définir des fonctions par nous même sera plus compliqué.

Avant de nous lancer dans les détails de la définition des fonctions voyons pourquoi définir nos propre fonctions est si important :

  • modularité Les fonctions séparent les programmes longs en parties plus petites, rendant le code plus facile à utiliser et plus lisible. Une fois que nous aurons appris à dessiner un vaisseau spatial et que nous l'aurons placé dans une fonction, nous pourrons appeler cette fonction à tous les endroits où c'est nécessaire sans se soucier de la façon dont on dessine un vaisseau spatial.
  • réutilisabilité Les fonctions nous permettront de réutiliser du code sans avoir à le réécrire. Si par exemple nous désirons faire un Space Invaders à deux joueurs, nous devrons dessiner le vaisseau deux fois, nous pourrons appeler la fonction de dessin du vaisseau deux fois plutôt que de copier-coller le code.

Dans ce cours, nous réviserons certains de nos anciens programmes, écrits sans fonction (quelle honte), et nous essayerons de montrer la puissance de l'approche par fonction pour la modularité et la réutilisabilité. Nous en profiterons pour mettre l'accent sur la distinction entre les variables globales et locales, puisque les fonctions sont des blocs indépendants de code qui requerront l'utilisation de telles variables locales. Enfin, nous continuerons à suivre les aventures de Chip mais avec des fonctions.

Note : Les fonctions sont souvent nommées différemment suivant les langages de programmation et les usages. On les appelles parfois procédures ou méthodes. Certains langages font une distinction nette entre les procédures (réaliser une tâche) et les fonctions (calculer une valeur). Pour l'instant, nous utiliserons le terme fonction pour qualifier globalement tous ces éléments. En Java le langage sous-jacent de Processing, les fonctions sont appelées méthodes en raison de la nature orientée-objet du langage. Nous étudierons les méthodes par la suite.

Définir des fonctions

Les fonctions que nous utilisons dans Processing, comme line(0, 0, 200, 200); par exemple, sont des fonctions que l'on a qualifié de prédéfinies. Cependant, elles ont été écrites par un programmeur comme vous. L'une des forces de Processing est sa bibliothèque de fonctions prédéfinies particulièrement pratiques, que nous avons commencé à explorer. Apprenons maintenant à étendre cette bibliothèque !

Une définition de fonction (ont dit aussi souvent déclaration de fonction) possède trois parties :

  1. le type de retour,
  2. le nom de la fonction,
  3. les arguments.

Cela ressemble à ceci :

typeDeRetour nomDeLaFonction(arguments) {
  // Code dans le corps de la fonction...
}

Déjà-vu ? si cela vous donne une impression de déjà-vu c'est normal puisque en fait setup() et draw() sont des fonctions ! Nous définissons donc des fonctions depuis longtemps, sauf que ces dernières sont faites pour être appelées automatiquement par Processing, et non par nous.

Pour le moment, focalisons nous sur le nom des fonctions, nous nous occuperons du type de retour et des arguments par la suite. Voici un exemple :


Exemple 11.1. Définir une fonction.

void dessineUnCercleNoir() {
  fill(0);
  ellipse(50, 50, 20, 20);
}

Cette fonction très simple dessine un cercle noir à une position prédéfinie de l'écran (50, 50). Son nom dessineUnCercleNoir est libre, mais choisi de façon à être significatif. Et le corps de la fonction, entre accolades, ne fait que deux lignes.

Il est important de noter que ceci n'est que la définition de la fonction. Le code ne sera exécuté que lorsque la fonction sera effectivement appelée dans une partie du programme qui est elle-même exécutée. Cela est réalisé en écrivant le nom de la fonction, c'est à dire en l'appelant, comme on peut le voir dans l'exemple suivant :


Exemple 11.2. Appeler une fonction.

void draw() {
  background(255);
  dessineUnCercleNoir();
}

Exercice 11.1. Écrivez une fonction qui affiche Chip (ou votre propre créature). Appelez cette fonction à l'intérieur de draw().

void setup() {
  size(200, 200);
}

void draw() {
  background(0);
  ___
  ___
  ___ ...
}

___ ___ ___ {
  ___
  ___
  ___ ...
}

Modularité

Nous allons reprendre l'exemple de la balle rebondissante du cours 8 mais en le réécrivant avec des fonctions. Ceci nous permettra d'illustrer la technique de séparation du code en parties modulaires. Commençons par réviser le code que nous avions écrit, et découpons le en parties :


Exemple 8.1. Balle rebondissante, exemple repris du cours 8, les commentaires indiquent différentes parties du code.

int r = 25;
int x = r;
int y;
int v = 1;

void setup() {
  size(200, 200);
  y = height / 2;
}

void draw() {
  background(255);
  // 1. Dessiner la balle.
  ellipseMode(RADIUS);
  stroke(0);
  fill(175);
  ellipse(x, y, r, r);
  // 2. Bouger la balle.
  x += v;
  // 3. Nous avons atteint un bord, inverser la vitesse.
  if (x < r || x > width - r) {
    v = -v;
  }
}

Une fois que le découpage est réalisé, nous pouvons diviser le code en fonctions. Nous prenons les différentes parties de draw() et les plaçons dans des définitions de fonctions aux noms appropriés. Enfin nous appelons ces fonctions dans draw(). En général, par convention, les définitions de fonction sont placées après draw() :


Exemple 11.3. Une balle rebondissante, modulaire.

int r = 25;
int x = r;
int y;
int v = 1;

void setup() {
  size(200, 200);
  y = height / 2;
}

void draw() {
  background(255);
  dessiner(); // 1.
  bouger();   // 2.
  rebondir(); // 3.
}

// Fonction qui affiche une balle.
void dessiner() {
  ellipseMode(RADIUS);
  stroke(0);
  fill(175);
  ellipse(x, y, r, r);    
}

// Fonction qui bouge la balle.
void bouger() {
  x += v;    
}

// Fonction qui fait rebondir la balle.
void rebondir() {
  if (x < r || x > width - r) {
    v = -v;
  }    
}

Voyez-vous comment draw() est devenue simple et facile à lire ? Son code est réduit à des appels de fonctions. Les détails du mouvement et du dessin sont placés dans des définition de fonctions appropriées. L'un des bénéfices de cette version concerne votre santé mentale ! En effet, si vous avez écrit un tel code juste avant de prendre un mois de vacances dans votre destination préférée, les plaines du Kazakhstan occidental, lorsque vous revenez, ce dernier est facile à relire et comprendre. Par exemple durant vos longues randonnées kazakh, vous avez décidé que la balle devait être rouge. Il suffit alors de changer la fonction dessiner() sans avoir besoin de relire tout le code.

Par exemple, essayez de remplacer votre fonction dessiner() par celle-ci :

void dessiner() {
  rectMode(RADIUS);
  noFill();
  stroke(0);
  rect(x, y, r, r);
  fill(175);
  rect(x - r / 2, y - r / 4, 2, 2);
  rect(x + r / 2, y - r / 4, 2, 2);
  line(x - r / 2, y + 4, x + r / 2, y + 4);
}

Un autre bénéfice sera que nous pourrons plus facilement déboguer les programmes. Par exemple, en supposant que notre balle rebondissante ne fonctionne pas bien, nous aurons l'option de désactiver ou d'activer des parties du programme facilement, il suffira de mettre en commentaire l'appel des fonctions :

void draw() {
  background(0);
  dessiner();
  // bouger();
  // rebondir();
}

Cela nous permet de déduire plus facilement la position d'un problème dans un code qui ne fonctionne pas comme on s'y attend.


Exercice 11.3. Prenez n'importe quel programme que vous avez écrit précédemment et modularisez-le avec des fonctions.


La fonction utile

Source CommitStrip

Arguments

Nous avions laissé de côté les arguments afin de mieux comprendre les bases des fonctions. Cependant les fonctions sont bien plus qu'un simple moyen de couper le code en parties. Il est temps de s'intéresser aux arguments des fonctions.

Les arguments sont des valeurs passées à une fonction. Vous pouvez y penser comme les entrées de l'algorithme développé par la fonction, ou comme les conditions nécessaires afin que la fonction puisse s'exécuter. Par exemple au lieu de simplement dire bouger, nous pourrions dire bouger de n étapes, où n est l'argument.

Quand nous affichons une ellipse avec Processing, nous devons spécifier les détails de l'ellipse. Nous ne pouvons pas simplement dire "dessine une ellipse", il faut spécifier sa position et sa taille. Ce sont les arguments de la fonction ellipse() que nous connaissons désormais bien.

Essayons de réécrire dessineUnCercleNoir() avec un argument :

void dessineUnCercleNoir(int diametre) {
  fill(0);
  ellipse(50, 50, diametre, diametre);
}

Un argument est donc simplement une variable que l'on déclare entre les parenthèses de la définition de fonction. Cette variable est une variable locale, a utiliser dans la fonction, et seulement dans cette fonction. Ainsi, ici, le cercle noir sera dimensionné grâce à l'argument placé entre parenthèses :

dessineUnCercleNoir(16);    // Un cercle noir de 16 pixels de diamètre.
dessineUnCercleNoir(32);    // Un cercle noir de 32 pixels de diamètre.

En considérant notre exemple de balle rebondissante, nous pourrions par exemple réécrire bouger() de façon à inclure un argument "facteur de vitesse" :

void bouger(int facteurVitesse) {
  x += (v * facteurVitesse);
}

Ainsi, nous pouvons faire bouger la balle deux fois plus vite :

bouger(2);

ou cinq fois plus vite :

bouger(5);

Ou encore passer un facteur à partir d'une autre variable ou une expression arithmétique :

bouger(mouseX / 10);

Les arguments nous seront très utiles pour créer des fonction plus réutilisables et plus flexibles. Nous allons essayer de montrer cela avec un code permettant de dessiner une collection de formes et en examinant comment les fonctions nous permettent de dessiner plusieurs versions de ces formes sans avoir à réécrire du code.

Considérez le dessin suivant qui ressemble à une voiture vue de dessus :

int x = 100;
int y = 100;
int taille = 64;            // taille de la voiture
int decalage = taille / 4;    // position des routes par rapport à la voiture.

size(200, 200);
background(255);

// Le corps de la voiture.
rectMode(CENTER);
stroke(0);
fill(175);
rect(x, y, taille, taille / 2);

// Les roues.
fill(0);
rect(x - decalage, y - decalage, decalage, decalage / 2);
rect(x + decalage, y - decalage, decalage, decalage / 2);
rect(x - decalage, y + decalage, decalage, decalage / 2);
rect(x + decalage, y + decalage, decalage, decalage / 2);

Allez...

Et pour faire une seconde voiture, nous devons répéter le même code avec des valeurs différentes :

x = 50;
y = 50;
taille = 24;            // taille de la voiture
decalage = taille / 4;    // position des routes par rapport à la voiture.

// Le corps de la voiture.
rectMode(CENTER);
stroke(0);
fill(175);
rect(x, y, taille, taille / 2);

// Les roues.
fill(0);
rect(x - decalage, y - decalage, decalage, decalage / 2);
rect(x + decalage, y - decalage, decalage, decalage / 2);
rect(x - decalage, y + decalage, decalage, decalage / 2);
rect(x + decalage, y + decalage, decalage, decalage / 2);

...en voiture...

Vous voyez venir la solution ! Si on veut une troisième voiture il faudrait à nouveau copier-coller du code. Il serait donc plus intéressant à la place d'utiliser une fonction avec des arguments pour la position, la taille et éventuellement la couleur. Voici un exemple :


Exemple 11.4. Une voiture modulaire.

void dessineVoiture(int x, int y, int taille, color c) {
  int decalage = taille / 4;

  // Le corps de la voiture.
  rectMode(CENTER);
  stroke(0);
  fill(c);
  rect(x, y, taille, taille / 2);

  // Les roues.
  fill(200);
  rect(x - decalage, y - decalage, decalage, decalage / 2);
  rect(x + decalage, y - decalage, decalage, decalage / 2);
  rect(x - decalage, y + decalage, decalage, decalage / 2);
  rect(x + decalage, y + decalage, decalage, decalage / 2);
}

Ensuite dans draw() nous pouvons appeler dessineVoiture() trois fois, en passant des paramètres différents :

void setup() {
  size(200, 200);
}

void draw() {
  background(0);
  dessineVoiture(100, 100, 64, color(200,200,0));
  dessineVoiture(50, 75, 32, color(0, 200, 100));
  dessineVoiture(80, 175, 40, color(200, 0, 0));
}

...Simone


Techniquement, les arguments sont les variables qui sont déclarées à l'intérieur des parenthèses lors de la définition d'une fonction. Par exemple dessineVoiture(int x, int y, int taill, color c). Les paramètres sont les valeurs passées à la fonction quand cette dernière est appelée. Par exemple dessineVoiture(80, 175, 40, color(100, 0, 100)). La différence sémantique est plutôt triviale. C'est pourquoi nous ne devrions pas être trop gênés par la confusion possible entre les deux mots de temps en temps.

En revanche, il est important de nous focaliser sur la capacité à passer des paramètres. Nous devons être à l'aise avec cette technique. Voici une comparaison : imaginez une belle journée ensoleillée, vous jouez au frisbee avec un ami dans un parc. Vous avez le frisbee. Vous (le programme principal) appelez la fonction (votre ami) et lui passez le frisbee (les arguments). Votre ami (la fonction) a maintenant le frisbee (l'argument) et il peut l'utiliser comme il le désire (le code dans la fonction).

Important

  • Vous devez passer le même nombre de paramètres que les arguments définis dans une fonction.
  • Quand un paramètre est passé il doit correspondre au type de l'argument correspondant. Un entier doit être passé pour un entier, un flottant pour un flottant, etc.
  • La valeur passée en paramètre peut être une valeur litérale (20, 5, 42...), une variable (x, y, etc.), ou le résultat d'une expression (8 + 3, 4 * x/2, random(0,10), etc.).
  • Les arguments se comportent comme des variables locales au bloc de la fonction, ils ne sont accessibles que dans la fonction.

Exercice 11.4. Voici le code de notre balle rebondissante, mais la balle s'est transformée en voiture ! Il manque quelques instructions pour faire fonctionner le programme. Remplissez-les. Notez au passage que les variables régissant le déplacement de la balle/voiture sont renommées en globalX et globalY pour ne pas occasionner de confusion avec les variables x et y en argument de dessineVoiture().

int globalX = 0;
int globalY;
int globalTaille = 50;
int v = 1;

void setup() {
  size(200, 200);
  globalX = globalTaille / 2;
  globalY = height / 2;
}

void draw() {
  background(0);
  ___
  ___
  ___
}

void dessineVoiture(int x, int y, int taille, color c) {
  int decalage = taille / 4;

  // Le corps de la voiture.
  rectMode(CENTER);
  stroke(0);
  fill(c);
  rect(x, y, taille, taille / 2);

  // Les roues.
  fill(175);
  rect(x - decalage, y - decalage, decalage, decalage / 2);
  rect(x + decalage, y - decalage, decalage, decalage / 2);
  rect(x - decalage, y + decalage, decalage, decalage / 2);
  rect(x + decalage, y + decalage, decalage, decalage / 2);
}

void bouger(int facteurVitesse) {
  globalX += (v * facteurVitesse);    
}

void rebondir() {
  if (globalX < (globalTaille / 2) || globalX > (width - (globalTaille / 2))) {
    v = -v;
  }    
}

Exercice 11.5. Utilisez la fonction dessineVoiture() pour faire une voiture qui se déplace avec la souris.


Exercice 11.6. Définir une fonction dessineRoue() permettant de simplifier le code de dessin de roues :

dessineRoue(x, y, -1, -1, decalage);
dessineRoue(x, y,  1, -1, decalage);
dessineRoue(x, y, -1,  1, decalage);
dessineRoue(x, y,  1,  1, decalage);

Exercice 11.7. Reprenons l'exercice 10.5 en essayant de l'écrire de façon modulaire. Voici une fonction qui dessine un carré de couleur aléatoire :

void dessineCarre(int x, int y, int taille) {
  noStroke();
  fill(random(255));
  rect(x, y, taille, taille);
}
  1. En utilisant la fonction dessineCarre(), écrire une fonction permettant de dessiner une ligne de la grille :
void dessineLigne(int y, int tailleCarre) {
  ...
}
  1. En utilisant la fonction dessineLigne(), écrire une fonction permettant de dessiner la grille :
void dessineGrille(int tailleCarre) {
  ...
}
  1. Appeler la fonction dessineGrille() dans draw().

Exercice 11.8. Voici une fonction qui dessine un œil :

void dessineOeil(float x, float y, float taille, color couleur) {
  stroke(0);
  fill(255);
  ellipse(x, y, taille, taille);
  float d = taille / 4 / dist(x, y, mouseX, mouseY);
  noStroke();
  fill(couleur);
  ellipse(x + (mouseX - x) * d, y + (mouseY - y ) * d, taille / 2, taille / 2);
}

Utilisez-la pour dessiner une créature qui a deux (ou plus) yeux.


Généralisons!

Source xkcd

⚠️ **GitHub.com Fallback** ⚠️