cours14 - sbalev/processing101 GitHub Wiki

Il faut faire le ménage de temps en temps

Nous avons vu qu'on peut instancier plusieurs objets d'une classe :

void setup() {
  size(800, 800);
  for (int i = 0; i < 100; i++) {
    Voiture v = new Voiture(random(0, width), random(0, height), random(10),
      color(random(255), random(255), random(255)), random(20, 50));
    v.dessiner();
  }
}

void draw() {
}

Nous verrons bientôt comment stocker tous ces objets pour pouvoir les utiliser par la suite.

De la même façon, vous pouvez utiliser autant de classes que vous voulez dans le même sketch. Si vous programmez un jeu, vous pouvez faire une classe Joueur, une classe Ennemi une classe Projectile etc. Nous allons montrer cette possibilité avec un exemple légèrement plus complexe en utilisant l'approche objet.

Dans quels cas faut-il utiliser la POO ? La réponse courte est toujours. Les objets permettent d'organiser les concepts à l'intérieur d'une application de façon modulaire et réutilisable. Mais il n'est pas obligatoire de commencer chaque projet Processing en utilisant des objets, surtout quand on apprend. Processing permet de « gribouiller » rapidement des idées en n'utilisant que nos vieux amis setup() et draw(). Mais quand vous avez fixé vos idées et quand votre projet commence à grandir, il est indispensable de prendre le temps nécessaire et de réorganiser votre code. Il est parfaitement acceptable de passer une partie significative de votre temps dans ce processus (souvent appelé refactoring) sans changer le fonctionnement de votre programme. Le temps perdu sera moins que le temps que vous allez gagner par la suite.

Good code

Source xkcd

Chip est beaucoup plus qu'un simple objet

Reprenons l'exemple 4.6 et essayons de le réorganiser en utilisant des objets. Tout d'abord faisons une brève description en français du fonctionnement de ce sketch.

Chip se déplace de façon autonome en direction d'une cible. La cible change de position lorsque l'utilisateur clique avec la souris.

Quels objets pouvons-nous identifier dans cette description ? Facile, il y a Chip et la cible.

La cible

Quelles sont les propriétés de la cible ?

  • position (x, y)
  • taille
  • couleur

Quelles sont ses fonctionnalités ?

  • se dessiner dans la fenêtre
  • se déplacer à une position donnée

Cela nous donne les attributs et les méthodes de notre classe Cible :

class Cible {
  float x, y;
  float taille;
  color couleur;

  Cible(float x, float y, float taille, color couleur) {
    this.x = x;
    this.y = y;
    this.taille = taille;
    this.couleur = couleur;
  }

  void dessiner() {
    stroke(couleur);
    strokeWeight(taille / 10);
    line(x - taille / 2, y - taille / 2, x + taille / 2, y + taille / 2);
    line(x - taille / 2, y + taille / 2, x + taille / 2, y - taille / 2);    
  }

  void deplacer(float nouveauX, float nouveauY) {
    x = nouveauX;
    y = nouveauY;
  }
}

Notez bien que tout comme le constructeur, les autres méthodes peuvent avoir des arguments. Ici notre méthode deplacer() a deux arguments pour préciser la nouvelle position de la cible.

Avant de nous occuper avec la suite, faisons un petit sketch pour tester si tout fonctionne bien. Nous allons créer deux cibles, une qui se déplace tout le temps avec la souris et une autre qui se déplace uniquement quand l'utilisateur clique avec la souris.


Exemple 14.1. Test de la classe Cible

Cible cibleSouris, cibleClick;

void setup() {
  size(400, 400);
  cibleSouris = new Cible(0, 0, 25, color(0, 126, 198));
  cibleClick = new Cible(width / 2, height / 2, 50, color(255, 144, 0));
}

void draw() {
  background(255);
  cibleClick.dessiner();
  cibleSouris.deplacer(mouseX, mouseY);
  cibleSouris.dessiner();
}

void mousePressed() {
  cibleClick.deplacer(mouseX, mouseY);
}

Chip

Propriétés de Chip :

  • position (x, y)
  • vitesse (ce sera un nombre, nous allons déterminer la direction en fonction de la position vers laquelle se déplace Chip)
  • largeur (la hauteur dépendra de la largeur pour garder Chip bien proportionné)
  • couleur (du corps)

Fonctionnalités de Chip :

  • se dessiner
  • avancer vers une position donnée

Transformons cette description en classe :

class Chip {
  float x, y;
  float vitesse;
  float largeur;
  color couleur;

  Chip(float x, float y, float vitesse, float largeur, color couleur) {
    this.x = x;
    this.y = y;
    this.vitesse = vitesse;
    this.largeur = largeur;
    this.couleur = couleur;
  }

  void dessiner() {
    // l'unité de mon papier à carreaux
    float u = largeur / 7;

    // le corps
    stroke(0);
    strokeWeight(1);
    fill(couleur);
    rectMode(CENTER);
    rect(x, y + 3.5 * u, largeur, largeur);

    // les roues
    fill(255);
    ellipseMode(CENTER);
    ellipse(x - 2 * u, y + largeur, 2 * u, 2 * u);
    ellipse(x + 2 * u, y + largeur, 2 * u, 2 * u);

    // la tête
    fill(couleur);
    arc(x, y - u / 2, largeur, largeur, -PI, 0, CHORD);

    // la caméra
    fill(63, 0, 0);
    ellipse(x, y - 2 * u, 2 * u, 2 * u);

    // l'antenne
    strokeWeight(3);
    line(x, y - 4 * u, x, y - 8 * u);
  }

  void avancerVers(float cibleX, float cibleY) {
    float d = dist(x, y, cibleX, cibleY);
    if (d >= vitesse) {
      x += vitesse * (cibleX - x) / d;
      y += vitesse * (cibleY - y) / d;
    }
  }
}

Et encore une fois, faisons un petit test pour nous assurer que tout fonctionne bien.


Exemple 14.2. Chip orienté objet

Chip chip;

void setup() {
 size(400, 400);
 chip = new Chip(0, 0, 2, 70, 191);
}

void draw() {
 background(255);
 chip.avancerVers(width / 2, height / 2);
 chip.dessiner();
}

Chip suit la cible

Maintenant on est prêt à intégrer nos deux classes dans le même sketch. Grâce à la POO, le code du programme principal est très court est lisible. On peut prendre le code des classes tel quel et le réutiliser dans d'autres applications ou y ajouter des fonctionnalités en fonction de nos besoins.


Exemple 14.3. Chip suit la cible

Chip chip;
Cible cible;

void setup() {
  size(600, 600);
  chip = new Chip(0, 0, 1, 70, 191);
  cible = new Cible(width / 2, height / 2, 40, color(255, 128, 0));
}

void draw() {
  background(255);
  cible.dessiner();
  chip.avancerVers(cible.x, cible.y);
  chip.dessiner();
}

void mousePressed() {
  cible.deplacer(mouseX, mouseY);
}

À noter l'utilisation de cible.x et cible.y pour récupérer la position de la cible et la passer à Chip. On utilise la même notation pour les méthodes.

  • cible.dessiner() signifie « appeler la méthode dessiner() de l'objet cible ».
  • cible.x signifie « la variable x de l'objet cible»

Il n'est pas toujours une bonne idée d'accéder directement aux champs d'un objet. Il existent des moyens pour restreindre cet accès. Nous reparlerons de ce problème et de l'encapsulation plus tard.

Et les LEDs ?

Nous les avons gardé pour la fin car nous voulons montrer une chose importante : les classes sont des types comme tous les autres. On peut utiliser une variable objet presque partout où on peut utiliser une variable de type primitif. On peut avoir des arguments de type objet, par exemple on peut écrire :

class Chip {
  ...
  void avancerVers(Cible cible) {
    avancerVers(cible.x, cible.y);
  }
}

Nous avons maintenant deux méthodes différentes qui portent le même nom mais qui ont des arguments différentes (cela permet de faire la distinction entre les deux). On voit également qu'on peut appeler une méthode à partir d'une autre.

Les objets peuvent également être des variables d'instance d'autres objets. De la même façon que Chip a une position, une vitesse, etc., il peut avoir des LEDs. En supposant qu'on a une classe Led, on peut écrire :

class Chip {
  Led ledR, ledG, ledB;
  ...
}

Quelles seront les propriétés et les fonctionnalités d'une LED ? En général une LED ne se balade pas toute seule, elle est attachée à un autre objet. C'est pourquoi elle n'aura pas sa propre position. En revanche elle aura une taille, une couleur et un état (allumée ou éteinte). Une LED doit pouvoir se dessiner. Mais où si elle n'a pas de position ? C'est Chip qui va lui dire où par rapport à sa propre position. Nos LEDs doivent également pouvoir s'allumer et s'éteindre. On ajoutera également une méthode permettant de vérifier si la LED est allumée.

class Led {
  float taille;
  color couleur;
  boolean on;

  Led(float taille, color couleur) {
    this.taille = taille;
    this.couleur = couleur;
    on = false;
  }

  void dessiner(float x, float y) {
    noStroke();
    if (on) {
      fill(couleur);
    } else {
      fill(63);
    }
    rectMode(CENTER);
    rect(x, y, taille, taille);
  }

  void allumer() {
    on = true;
  }

  void eteindre() {
    on = false;
  }

  boolean estAllumee() {
    return on;
  }
}

Pour casser un peu notre routine de constructeurs, nous avons décidé de ne pas initialiser tous les champs avec des arguments. Une nouvelle LED est toujours éteinte. On a une méthode permettant de l'allumer si l'on veut.

Maintenant il faut modifier le code de Chip pour qu'il gère ses LEDs. Il doit les instancier et dessiner. En plus, on va lui ajouter une nouvelle fonctionnalité : il va avoir toujours une de ses LEDs allumée et les deux autres éteintes. Quand on lui demande de changer la LED, il va éteindre celle qui est allumée et allumer la suivante.

class Chip {
  ...
  Led ledR, ledG, ledB;

  Chip(float x, float y, float vitesse, float largeur, color couleur) {
    ...
    // instancier les LEDs
    ledR = new Led(largeur / 7, color(255, 0, 0));
    ledG = new Led(largeur / 7, color(0, 255, 0));
    ledB = new Led(largeur / 7, color(0, 0, 255));
    ledR.allumer();
  }

  void dessiner() {
    ...
    // les LEDs
    ledR.dessiner(x - 2 * u, y + 2.5 * u);
    ledG.dessiner(x, y + 2.5 * u);
    ledB.dessiner(x + 2 * u, y + 2.5 * u);
  }
  ...

  void avancerVers(Cible cible) {
    avancerVers(cible.x, cible.y);
  }

  void changerLed() {
    if (ledR.estAllumee()) {
      ledR.eteindre();
      ledG.allumer();
    } else if (ledG.estAllumee()) {
      ledG.eteindre();
      ledB.allumer();
    } else if (ledB.estAllumee()) {
      ledB.eteindre();
      ledR.allumer();
    }
  }
}

Nous allons voir bientôt comment on peut écrire la dernière méthode de façon beaucoup plus concise et élégante.

Pour voir les LEDs de Chip clignoter, nous n'avons qu'une instruction à ajouter dans le programme principal.


Exemple 14.4. Chip suit la cible et clignote

Chip chip;
Cible cible;

void setup() {
  size(600, 600);
  chip = new Chip(0, 0, 1, 70, 191);
  cible = new Cible(width / 2, height / 2, 40, color(255, 128, 0));
}

void draw() {
  background(255);
  cible.dessiner();
  chip.avancerVers(cible);
  chip.dessiner();
  if (frameCount % 60 == 0) {
    chip.changerLed();
  }
}

void mousePressed() {
  cible.deplacer(mouseX, mouseY);
}

Le code complet en 4 fichiers est disponible dans le dépôt principal.


Mise en pratique


Exercice 14.1.

Reprenez un de vos anciens projets et faites un refactoring de votre code.

  • Commencez par organiser le code en fonctions si ce n'est pas déjà fait.
  • Allez plus loin et faites au moins deux classes.
  • Ajoutez des constructeurs avec arguments à vos classes et instanciez deux ou plusieurs objets avec des propriétés différentes.
  • Animez ces objets depuis le programme principal et faites-les interagir.

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