Les enseignements - Plateforme Temps Réel - Atelier Hypermédia - Processing -

processing[14][0] = « Programmation orientée-objet (version sympathique) » ;

Pour comprendre le fonctionnement — et l’intérêt — de la programmation dite «  orientée-objet  », commençons avec quelques exemples concrets, avant d’attaquer la théorie et le syntaxe de cette forme de programmation à la fois simple et complexe (comme tout, n’est-ce pas ?).

La chose

Voici une chose un peu nerveuse que vous pouvez copier et animer dans votre copie de Processing :


// avant de dessiner, définir la position x,y de la chose
float x = 50, y = 50;

void draw() {
 
 // fond blanc à chaque instant
 background(255);
 
  // ajouter -1, 0, ou +1 à la position x de la chose
 x += random(-1,1);
 
  // ajouter -1, 0, ou +1 à la position y de la chose
 y += random(-1,1);
 
 // dessiner la chose
 ellipse(x, y, 5, 5);
 
}

Cette chose a été programmée avec un minimum de concepts de base pour pouvoir se concentrer plus tard sur l’essentiel, la programmation d’«  objets  ». Nous allons le rendre plus sophistiqué par la suite, mais au départ commençons plutôt avec une chose plutôt bête : un cercle qui se déssine sur l’écran 60 fois par seconde en changéant toutes les 1/60 seconde la position x et/ou y de cette chose. Notez que la chose est tellement bête que si vous la laisser frétiller assez longtemps elle sortira par un des bords de l’écran sans jamais revenir dans l’éspace visible de votre sketch.

La Chose

Comment faire si nous ne voulons non pas une seule chose, mais deux ? Si vous avez lu le chapitre sur les listes vous allez peut-être répondre : « Simple, mon cher Watson. Nous allons créer une liste pour chaque position x, et une liste pour chaque position y  ». Et vous aurez raison. C’est une possibilité. Ce type de Sketch Processing ressemblerait à quelque chose comme ceci :


float[] x = {10.0, 25.0, 42.0, 50.0, 55.0};
float[] y = {11.0, 66.0, 73.0, 31.0, 42.0};

void draw() {
   for(int i= 0; i < x.length; i++) {
       x[i] += random(-1,1);
       y[i] += random(-1,1);
       ellipse(x[i], y[i], 5, 5);
   }
}

mais il y a mieux. Dès que les figures dessinées par ce code se complexifieront, il deviendront quasi-impossible de programmer de cette façon sans rendre le code illisible et introduire des erreurs presque partout. Trust me ; je sais de quoi je parle : vos programmes deviendront vite très pénibles.

Comment faire, alors ? À la place de l’utilisation des listes pour gérer tous les paramètres de toutes les figures qui doivent apparaître dans le Sketch, vous pouvez créer la «  classe  » d’une Chose qui représente une figure idéale, et à partir de cette classe «  Chose  », créer autant d’objets-choses que vous voulez. Voici l’exemple ci-haut, transformé de cette façon :


Chose une_chose = new Chose();
Chose une_autre = new Chose();

void draw() {
 
 une_chose.bouger();
 
 une_chose.dessiner();
 une_autre.dessiner();
 
}


class Chose {

 // avant de dessiner, définir la position x,y de la chose (au milieu)
 float x = 50, y = 50;

 void bouger() {
   
   // fond blanc à chaque instant
   background(255);
   // ajouter -1, 0, ou +1 à la position x de la chose
   x += random(-1,1);
   // ajouter -1, 0, ou +1 à la position y de la chose
   y += random(-1,1);

 }
 
 void dessiner() {
   // dessiner la chose
   ellipse(x, y, 5, 5);
 }

}

L’idée ici, c’est que vous pouvez créer une sorte de plan (« class ») qui explique tout ce qu’une Chose sait faire — ainsi que toutes les propriétés dont la Chose a besoin pour les faire — puis créer autant de choses spécifiques (« new ») basées sur le plan la Chose générique. Vous faites le code de base une fois, puis vous dupliquez autant de fois que vous voulez les objets qui utiliseront ce code : du travail en moins. Les choses — ici nommées «  une_chose  » et «  une_autre  », sont toutes les deux de type «  Chose  » mais avec des comportements spécifiques. Chacun contient ses propres valeurs, même si les noms de ces valeurs sont les mêmes. Et chacun contient les mêmes comportements potentiels, mais peuvent être actionnés (ou pas actionnés) de façon totalement individuel.

Il est important de comprendre l’idée de cette dernière phrase et d’observer l’endroit dans le code où elle s’exprime. La première chose (une_chose) bouge, alors que le deuxième (une_autre) ne bouge pas. Par contre, les deux savent se dessiner sur l’écran. Si nous regardons dans le code, nous voyons tout de suite pourquoi : on a uniquement demandé à la première chose de bouger, alors qu’on a demandé aux deux de se dessiner :


void draw() {
 
 une_chose.bouger();
 
 une_chose.dessiner();
 une_autre.dessiner();
 
}

Le point

Dans l’orienté-objet, nous accédons aux méthodes et aux variables et utilisant le signe «  .  », c’est-à-dire un point. Le point permet d’accéder aux propriétés (les noms des objets) et aux actions (les verbes des objets). On peut d’ailleurs reconnaître la différence entre les deux par la présence (ou pas) de parenthèses autour du mot : ceux avec des parenthèses sont des méthodes et ceux sans parenthèses sont des variables. Nous accédons aux deux à travers le point.

Armé ces explications, regardez de nouveau le code et la façon dont il se comporte :

  • Au début nous créons deux objets basés sur le patron Chose («  new  »)
  • 60 fois par seconde (la vitesse par défaut) nous demandons :
    • à la première chose de bouger (« .bouger() »)
    • à la première chose puis à la deuxième chose de se dessiner dans le sketch (« .dessiner() »)
  • Ce comportement se répète jusqu’à ce que le Sketch se termine

Si je voulais donner une analogie plutôt foireuse, je pourrais imaginer une classe appelée « Professeur », avec les propriétés et actions suivantes :


class Professeur {

   int salaire = -1;
   boolean heureux = true;

   void parler() {
       println("blah blah blah blah");
       heureux = true;
       verifierSalaire();
   }

   void verifierSalaire() {
      if (salaire < 0) heureux = false;
   }

   boolean verifierEtat() {
       return(heureux);
   }

   void augmenter() {
       int laRealite = 0;
       int onPeutToujoursRever = 999;
       if ( laRealite > onPeutToujoursRever ) {
           salaire++;
           heureux = true;
       }
   }

}

À partir de ce code pas si loin de la vérité (est-ce le bon endroit ici pour faire la manche ?), je pourrais ensuite créer un nouveau professeur, demander à ce professeur d’enseigner de temps en temps, et puis de temps en temps feigner un effort sur son salaire.


Professeur douglas = new Professeur();
String jour = "jeudi";

void draw() {
   if (jour == "jeudi") {
       douglas.parler();
       if (douglas.verifierEtat() == false) douglas.augmenter();
   }
}

Si vous avez compris le code de notre classe (Professeur) et le comportement de notre objet (douglas), vous avez vite compris que les rêves sont toujours plus grands que la réalité, et que le salaire n’est jamais augmenté de notre pauvre professeur. mais au moins il est heureux tant qu’il continue d’enseigner.

Vous avez peut-être aussi noté un détail important : j’ai écrit le nom de la classe « Professeur » avec un majuscule, alors que j’ai écrit le nom de l’objet qui dépend de cette classe, « douglas », avec un minuscule. Bien que vous puissiez écrire vos noms de classes avec ou sans majuscule, je vous recommande néanmoins de toujours écrire les noms des classes (= génériques) avec un majuscule, et les noms des objets dépendants (= spécifiques) avec des minuscules. Cela permet de lire tout de suite dans le code la partie « schéma » ou « plan » et la partie des maisons spécifiques construites par ces plans. Cela permet de lire la partie où nous définissons que toutes les maisons ont un toit, mais que maison « sansSoucis » aura un toit beige et que la maison « cameSuffit » aura un toit rose.

Déclaration d’une classe

Un mot sur la déclaration d’une classe, et celui d’un objet qui dépend de cette classe. Nous reviendrons sur la théorie de l’orienté-objet dans un cours futur, mais il est important de comprendre au moins le syntaxe des différentes parties de cette forme de programmation. Commençons avec la déclaration du plan qui décrira tout ce qu’un objet saura faire. C’est ce plan qui s’appelle une «  classe  ». Elle s’écrit toujours de la façon suivante :

nom de la classe + accolade d’ouverture + tout le code de la classe + accolade de fermeture

Tout ce qui est entre les deux accolades sera intégré dans les instructions de la classe.

On déclare une classe en donnant alors un nom à la classe, et en définissant toutes les variables et toutes les méthodes que les objets doivent contenir pour fonctionner correctement.

Déclaration d’un objet

Une fois que la classe est définie, nous pouvons créer autant d’«  objets  » de cette classe que nous voulons. Un «  objet  » n’est rien d’autre qu’une super-variable qui contient toutes les instructions et valeurs de la classe à l’intérieur d’elle. Il suffit pour cela de choisir un nom pour chaque objet et la classe qui doit être copié à l’intérieur de cette variable :

Intelligence

C’est un terme sur-exploité dans la littérature informatique, mais ici il est plutôt bien approprié : nos petites Choses se comportent de façon plutôt bête et il serait bien de leur donner un peu d’intelligence. Sans complexifier trop le programme, donnons au moins à nos petites bêtes la possibilité de se boucler comme si elles étaient dans un espace infini, en identifiant chaque rencontre des bords de l’écran.

Pour ce faire, il suffit de changer le code dans la classe «  Chose  » et tous les objets qui dépendent de cette classe ajouteront automatiquement ce don d’intelligence.

Voici notre programme avec cette modification. Notez aussi que j’ai ajouté quelques modifications pour rendre le Sketch plus sympathique sur le plan visuel et plus simple sur le modèle du comportement. Ceci nous aidera pour la suite du cours.


Chose une_chose = new Chose();
Chose une_autre = new Chose();

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

void draw() {
 // fond blanc à chaque instant
 background(255);
 // allez! au travail!
 une_chose.action();
 une_autre.action();
}


class Chose {

 // avant de dessiner, définir la position x,y de la chose (au milieu)
 float x = 100, y = 100;

 void action() {
   bouger();
   dessiner();
 }

 void dessiner() {
   fill(0,0,0,255); // couleur noir, opacité maximum
   ellipse(x, y, 5, 5);
 }

 void bouger() {
   // ajouter -1, 0, ou +1 à la position x de la chose
   x += random(-1,1);
   // ajouter -1, 0, ou +1 à la position y de la chose
   y += random(-1,1);
   // vérifier les bords. si trop loin, boucler
   if (x > width) x = 0;
   if (x < 0) x = width;
   if (y > height) y = 0;
   if (y < 0) y = height;
 }

}

Nous avons maintenant deux choses de type «  Chose  » qui tous les deux se baladent sur l’écran et sont capables de sentir si elles ont dépassé les bords de l’écran. Dans ce cas, elles réapparaissent de l’autre côté de l’écran, donnant l’impression d’être sur un espace bouclé, ou pseudo-sphérique (je dis «  pseudo  », car l’espace est évidemment carré et plat mais infini sur les deux axes de sa surface).

Je vous montre cet exemple pour que vous sentez la façon dont nous pouvons ajouter des comportements de plus en plus complexes à l’intérieur des objets, sans changer grand chose du programme de base. Cela nous permet non seulement d’ajouter de l’intelligence à nos objets, cela rend aussi notre approche de la programmation plus intelligente. Si nous pouvions séparer nos programmes en des sous-programmes non seulement sur le plan logique (cf. méthodes) mais également sur le plan des figures dessinées à l’intérieur du programme (c’est ça l’intérêt des objets), nous gagnerions autant en lisibilité qu’en maîtrise des comportements qui pourraient du coup devenir de plus en plus complexes — surtout dans l’interaction entre les figures. Pensez, pour ce dernier point, aux jeux vidéo.

Variation

C’est bien de pouvoir avoir plusieurs objets qu’on ne doit programmer qu’une seule fois. C’est aussi sympathique que chaque objet s’occupe de sa propre valeur x et y sans se confondre avec le x et y de son voisin. Cela permet de rationaliser la programmation, de rester avec des noms de variables les plus simples possibles, tout en retenant la singularité des objet qui en dépendent. mais c’est vite fatiguant d’avoir toujours le même comportement de tous nos objets, ou par exemple la même coloration. Actuellement nos deux choses de la class «  Chose  » se dessinent tous les deux de la même manière : avec du noir. Comment varier entre les deux objets, pour qu’une soit rouge, par exemple, et l’autre bleu ?

Pour ce genre de situation, il existe une fonctionnalité très pratique chez les classes qui s’appelle le «  constructeur  » de la classe. Qu’est-ce qu’un constructeur ? Et bien, c’est une partie de la classe qui est appelé chaque fois que nous créons un nouvel objet à partir de cette classe. Dans un langage plus humain nous pourrions dire que c’est la partie du programme qui dit, «  quand vous créez des objets de cette classe appelé Chose, effectuez les instructions suivantes pour individualiser les divers aspects de cet objet  ». Voici un exemple relativement simple pour illustrer ce principe :


void setup() {
 size(200,200);
 smooth();
 background(255);
 Rond rond_1 = new Rond();
 rond_1.dessiner();
 Rond rond_2 = new Rond();
 rond_2.dessiner();
}

class Rond {

 // les paramètres spécifiques à chaque objet de cette classe
 float x, y;
 color c;

 // le " constructeur "
 Rond() {
   // choisir une position au hasard
   x = random(width);
   y = random(height);
   // choisir une couleur au hasard
   c = color( random(255), random(255), random(255) );
 }

 // utiliser les paramètres de chaque objet pour les dessiner
 void dessiner() {
   fill(c);
   noStroke();
   ellipse(x, y, 10, 10);
 }

}

Regardez-bien le mouvement des flèches sur l’illustration : chaque fois qu’un nouveau «  Rond  » se crée, il effectue les instructions qui se trouvent dans le «  constructeur  » de la classe. Le constructeur de la classe a le même nom que la classe mais avec des parenthèses.

Dans notre illustration, la classe Rond crée non seulement des variables qui contiendront les positions x et y des Rond avec leurs color, mais elle donne à ces variables des valeurs singulières qui se traduisent en des positions et couleurs individuelles pour chaque Rond.

Arguments

C’est chouette de pouvoir individualiser plusieurs objets à partir d’une seule et même classe, mais comment ferions-nous si nous ne voulions pas des valeurs arbitraires («  random()  ») comme dans l’exemple précédent ?

La solution la plus simple réside dans le «  constructeur  » de la classe. Si vous retournez au cours processing<04><02> sur les variables, vous trouverez une illustration qui explique comment passer une ou plusieurs variables d’une partie de votre programme à une autre (cf. la méthode «  inverser()  »). De la même façon que vous pouvez «  passer  » des valeurs à une méthode, vous pouvez également «  passer  » des valeurs à un constructeur lors de la création d’un objet à partir de sa classe.

Si tout cela vous semble compliqué, voici un exemple pour clarifier la chose :

Dans l’exemple nous avons une classe qui génère des personnes à partir d’un modèle nommé «  Personne  ». Dans cette classe réside une variable qui s’appelle «  yeux  » et qui stocke une valeur colorée qui sera utilisée plus tard pour dessiner les yeux de la personne.

Lors de la création d’un objet à partir de cette classe, trois valeurs sont passées au constructeur, qui les utilise ensuite pour générer la couleur des yeux. Dans l’exemple, nous trouvons alors une personne appelé «  goober  » avec les valeurs r=255, v=0, b=0 ; c’est-à-dire avec des yeux rouges.

Listes d’objets

Ouf ! Nous avons des yeux bien rouges ! Et ce n’est pas pour rien. Nous avons traité beaucoup de matière aujourd’hui. mais avant de s’arrêter, je vous demande de vous accrocher encore un moment pour apprendre la dernière technique qui rassemblera tout ce que nous avons appris jusqu’ici : l’utilisation des listes dans la gestion des objets. L’intérêt d’utiliser des listes pour gérer vos objets, c’est que vous pouvez construire non pas un objet ou deux, mais plutôt des centaines, voire des milliers, tout en gardant le contrôle sur toutes ces bestioles.

Puisque nous parlons de bestioles, commençons par construire une bestiole que nous utiliserons comme patron pour toutes les autres bestioles que nous voulons animer (par une liste).

Une note sur la méthode : notre programme est plutôt longue. Pour simplifier la gestion de ce code, je vous recommande de couper votre programme en deux parties en utilisant le bouton onglets («  Tab  ») sur le côté droit de la fenêtre de Processing :

Les onglets sont juste une forme de rangement. En réalité, même si vous avez une dixaine d’onglets, Processing joue votre Sketch avec tous ces onglets mélangés. Vous pouvez d’ailleurs leur donner n’importe quel nom. Comme j’ai placé tout le code de la classe «  Bestiole  » dans un seul onglet, j’ai décidé alors de nommer cet onglet« Bestiole », c’est-à-dire avec le même nom que la classe qu’il contenait. Il n’y a pas d’obligation stricte sur ce point, mais quand vous commencez à avoir de multiples onglets pour des projets complexes, je vous recommande d’utiliser les onglets uniquement pour le rangement des classes, avec un seul onglet par classe, et avec un nom de l’onglet que correspond au nom de la classe qu’il contient.

Pour ces questions de rangement, notre programme est maintenant coupé en deux parties. Voici les deux parties :


Bestiole une_bestiole;
Bestiole une_autre_bestiole;

void setup() {
 
 size(200,200);
 colormode(HSB);      // HSB simplifie les couleurs aléatoires
 noStroke();
 smooth();            // formes anti-aliasées
 
 une_bestiole       = new Bestiole(100, 75, 30); // x, y, taille
 une_autre_bestiole = new Bestiole(50, 150, 15); // x, y, taille
}

void draw() {
 
 background(255);
 une_bestiole.draw();
 une_autre_bestiole.draw();
 
}

class Bestiole {

 color couleur;
 float x, y, taille;

 Bestiole(float depart_x, float depart_y, float t) {
   x = depart_x;
   y = depart_y;
   taille = t;
   couleur = color(random(255), 255, 255); // hue, saturation, brightness
 }

 void draw() {
   bouger();
   dessiner();
 }

 void bouger() {
     // bouger aléatoirement en fonction de sa taille
     x += random(-taille/10,taille/10);
     y += random(-taille/10,taille/10);
     // boucler sur les bords du Sketch
     if (x > width + taille) x = -taille;
     if (x < -taille) x = width + taille;
     if (y > height + taille) y = -taille;
     if (y < -taille) y = height + taille;
 }

 void dessiner() {

   // comme chaque bestiole change l'orientation, il faut mémoriser
   // l'orientation d'origine pour pouvoir y revenir après chaque dessin
   pushmatrix();

   // dessiner le corps
   fill(couleur);
   translate(x,y);
   // le corps est composé de plein de cercles
   for(int i=0; i<360; i+=30) {
     pushmatrix();
     rotate(radians(i));
     translate(random(-taille/10,taille/10), random(-taille/10,taille/10));
     ellipse(taille/2, 0, taille, taille);
     popmatrix();
   }

   // dessiner le visage
   fill(255);
   translate(random(-taille/20,taille/20), random(-taille/20,taille/20));
   ellipse(-taille/3, -taille/4, taille/3, taille/3);
   ellipse( taille/3, -taille/4, taille/3, taille/3);
   ellipse(        0,  taille/5, taille/4, taille/4);

   popmatrix(); // revenir à l'orientation d'origine
   
 }

}

Copiez ce code et essayez de le faire marcher dans votre copie de Processing. Si tout va bien vous devez avoir deux bestioles qui se baladent à l’intérieur de votre Sketch.

Dès que nous sommes contents du comportement de nos deux Bestioles, nous pouvons les multiplier au delà des deux ou trois objets que nous avons animés jusqu’ici. Pour ce faire, nous allons utiliser une liste pour pouvoir en avoir plusieurs. Si vous ne vous souvenez pas des listes, maintenant est le bon moment de retourner au cours précédent «  listes  ».

Comme nous avons vu dans le cours précédent, on peut créer une liste de vingt float en écrivant :

float[] mes_chiffres = new float[20];

Pour créer une série d’objets, c’est exactement le même syntaxe, mais au lieu d’écrire le mot float, vous le remplacerez par le nom de votre classe :

Bestioles mes_bestioles = new Bestiole[20];

N’oubliez pas ce que nous avons dit plus haut : un objet est une sorte de super-variable. Comme si vous avez inventé votre propre type de variable, au même titre que les int, float, et String. Au lieu de contenir un String, notre objet contient une Bestiole. Et comme un String peut être transformé en une liste de String, notre bestiole peut aussi être transformé en une liste de bestioles.

Voici le début de notre code modifié pour contenir une liste. Notez qu’il s’agit uniquement d’un changement des méthodes setup() et draw() ; nous n’avons plus besoin de toucher au comportement dans la classe Bestiole.


Bestiole[] bestioles = new Bestiole[20];

void setup() {

 size(300,300);
 colormode(HSB);      // HSB simplifie les couleurs aléatoires
 noStroke();
 smooth();            // formes anti-aliasées

 // pour chaque objet, choisir une taille &position de départ
 for(int i=0; i < bestioles.length; i++) {
   float taille = random(5,30);
   float depart_x = random(width);
   float depart_y = random(height);
   bestioles[i] = new Bestiole(depart_x, depart_y, taille);
 }
}

void draw() {

 background(255);

 for(int i=0; i < bestioles.length; i++) {
   bestioles[i].draw();
 }

}

Notre code est légèrement plus longue, mais beaucoup plus court que si on devait copier les mêmes instructions une vingtaine de fois. Notez également que nous avons besoin de traiter chaque objet à l’intérieur de la liste de bestioles séparément. L’utilisation de la liste est uniquement une façon de contenir plusieurs objets à l’intérieur d’une seule variable sans avoir besoin de nommer et écrire une vingtaine (ou cinquantaine, ou centaine) de noms de variables (objet_1.draw(), objet_2.draw(), objet_3.draw(), ...).


Si vous voulez voir une version en ligne du Sketch «  Bestioles  », vous le trouverez dans le Happy Code Farm ou en suivant ce lien : Bestioles.


ESAAix - Ecole supérieure d’art d’Aix-en-Provence - http://www.ecole-art-aix.fr