Dessiner et Animer des "chenilles"

Objectifs :

  • expérimenter avec la délégation et l'héritage en Java.
  • utiliser des tableaux en Java.

Dans ce TP, il s'agit d'écrire une application Java permettant d'animer une ou plusieurs "chenilles" se déplaçant dans une fenêtre sur l'écran. Chaque chenille se déplace de manière aléatoire et "rebondit" lorsque sa tête touche l'un des bords de la fenêtre.

1. Représentation d'une chenille

Une chenille est constituée d'une tête suivie d'une suite d'anneaux (figure 1).

figure 1: représentation d'une chenille composée de 10 anneaux
  • Un anneau est représenté par un cercle de rayon \(r\) (on propose \(r = 5\) comme valeur par défaut).
  • La tête de la chenille est représentée par un disque noir de rayon identique au rayon des anneaux.
  • Le corps de la chenille est constitué de \(n\) anneaux, numérotés de \(0\) à \(n-1\) ; l'anneau \(0\) est celui qui suit la tête et l'anneau \(n-1\) est celui de queue.
  • Le centre de l'anneau \(i\) \((i = 1, 2, ... , n - 1)\) est situé sur la circonférence de l'anneau \(i-1\) , le centre du premier anneau (anneau \(0\)) étant situé sur la circonférence de la tête.
  • Le cap de la chenille indique la direction dans laquelle se déplace la tête de la chenille. C'est l'angle entre l'axe \(Ox\) et le vecteur indiquant la direction de déplacement de la tête.

2. Animation d'une chenille

2.1 Création d'une chenille

chenille dans sa position initiale
figure 2: Position initiale d'une chenille dans la zone de dessin de l'application.

A sa création, la tête de la chenille est centrée au milieu de la zone de dessin, ses anneaux sont à gauche de la tête et alignés horizontalement (sur une ligne parallèle à l'axe \(Ox\)) (figure 2).

La zone de dessin est défini par un objet JPanel (voir javadoc JDK 8, JDK 17 ou JDK 21), l'équivalent d'un objet canvas en JavaScript. Le coin supérieur gauche de la zone dessin est l'origine du repère qui lui est associé. Les points-écran (pixels) à l'intérieur d'une zone de dessin sont à coordonnées entières. La taille d'une zone de dessin indique la taille de la zone d'affichage, elle ne tient pas compte du cadre de la fenêtre de l'application (objet JFrame) qui entoure celle-ci (voir javadoc JDK 8, JDK 17 ou JDK 21).

2.2 Avancée d'une chenille

A chaque étape de l'animation le déplacement de la chenille s'effectue de la manière suivante (figure 3) :

  • La tête se déplace d'une distance \(r\) (rayon des anneaux et de la tête) dans la direction de son cap,
  • chaque anneau prend la place de l'anneau précédent, l'anneau 0 prenant la place de la tête avant son déplacement.
figure 3: Déplacement d'une chenille (en gris les éléments de la chenille avant le déplacement,
en noir les éléments de la chenille après le déplacement)

A chaque nouveau déplacement élémentaire, le \(cap\) de la tête de la chenille est modifié de manière aléatoire (+/- 30°). Bien entendu la tête de la chenille ne doit pas sortir de la zone de dessin (la chenille rebondit) sur les bords de la zone dessin).


les anneaux de la chenille se décalent d'une position :
  - l'anneau n° i prend la position de l'anneau n° i-1 (i = N-1, N-2, ... ,1)
  - l'anneau n° 0 prend la position de la tête

la tête de la chenille effectue une déviation de cap d'un angle tiré au hasard 
              dans l'intervalle [-30°,+30°]

tant que le nouveau cap ne garantit pas que le prochain déplacement de la tête la maintiendra 
                entièrement dans la fenêtre* faire
  |
  |    dévier le cap de la tête de 10°
  |
fin tant que
 
la tête se déplace d'une distance r selon la direction définie par le nouveau cap

* la tête est entièrement dans la fenêtre si la distance de son centre aux bords de la fenêtre 
  est supérieure ou égale à son rayon
                        

3. Modélisation JAVA d'une Chenille

Pour modéliser une chenille en langage JAVA on décide de définir trois classes d'objets (voir le diagramme de classes UML figure 4):

  • une classe Anneau qui décrit les objets représentant chaque anneau de la chenille.
  • une classe Tête qui décrit l'objet représentant la tête de la chenille. La tête peut être vue comme un cas particulier d'anneau. La classe Tête sera donc définie en héritant de la classe Anneau (autrement dit la classe Tête sera une sous-classe de la classe Anneau).
  • une classe Chenille qui décrit un objet modélisant une chenille dans son ensemble et donc qui utilisera les services des classes Tête et Anneau.
figure 4: diagramme des classes de l'application Chenille.

Pour plus de détails vous pouvez consulter cette Vidéo explicative

3.1 La classe Anneau

Un Anneau est un objet défini par trois attributs (les coordonnées \(x\),\(y\) du centre du cercle le représentant et le rayon \(r\) de ce cercle) et qui est capable de se placer à une position donnée et de s'afficher. La spécification du constructeur et des méthodes des objets de type Anneau vous est donnée ci-dessous.


/**
* crée un Anneau en fixant sa position initiale et son rayon
* @param xInit abscisse initiale du centre de l'anneau
* @param yInit ordonnée initiale du centre de l'anneau
* @param r rayon de l'anneau
*/
public Anneau(int xInit, int yInit, int r) 

/**
* crée un Anneau en fixant sa position initiale, son rayon
* est fixé à une valeur par défaut qui vaut 10.
* @param xInit abscisse initiale du centre de l'anneau
* @param yInit ordonnée initiale du centre de l'anneau
*/
public Anneau(int xInit, int yInit) 


/**
* retourne abscisse du centre de l'anneau
* @return abscisse du centre de l'anneau
*/

public int getX()
/**
* retourne ordonnée du centre de l'anneau
* @return ordonnée du centre de l'anneau
*/
public int getY() 

/** positionne le centre de l'anneau en un point donné
* @param px abscisse du point
* @param py ordonnée du point
*/
public void placerA(int px, int py)

/** positionne l'anneau à la place d'un autre anneau
* @param a l'anneau dont le centre devient le centre de cet anneau (this)
*/
public void placerA(Anneau a)

/**
* affiche l'anneau en le matérialisant par un cercle noir
* @param g objet de classe java.awt.Graphics qui prend en charge la gestion
* de l'affichage dans la fenêtre de dessin
*/
public void dessiner(Graphics g)
                

Pour dessiner un cercle on utilisera le méthode drawOval de la classe java.awt.Graphics (pour en savoir plus sur son fonctionnement référez-vous à la javadoc de Graphics : JDK 8, JDK 17 ou JDK 21 ).

3.2 La classe Tête

La classe Tête est définie comme une sous-classe de la classe Anneau. En effet comme un anneau, une tête est définie par trois attributs (les coordonnées de son centre et son rayon) et est capable de se placer à une position donnée, de s'afficher et de s'effacer. Les différences sont les suivantes :

  1. La classe Tête enrichit la classe Anneau d'un nouvel attribut : cap un réel (double) qui définit la direction de déplacement de la tête.

  2. La position, le rayon et le cap initial d'une tête sont fixés à sa création (donnés en paramètre du constructeur). On peut également créer une Tete en spécifiant que sa position, son rayon a alors unevaleur par défaut identique à la valeur par défaut pour les anneaux, et son cap intial est nul.

  3. La méthode dessiner est redéfinie de manière à ne plus afficher un cercle selon le cas mais un disque noir (utiliser la méthode fillOval de java.awt.Graphics en lieu et place de drawOval).

  4. De plus trois nouvelles méthodes publiques sont ajoutées :
    • devierCap(double deltaC)
      qui permet de modifier le cap de la tête en lui ajoutant la déviation définie par le paramètre deltaC.

    • deplacerSelonCap()
      qui modifie le centre de la tête en lui appliquant un déplacement de longueur \(r\) dans la direction définie par le cap. Pour cela, on calcule la nouvelle position du centre de la tête à l'aide de la formule suivante :

      \(x' = x + r * cos(cap)\) et \(y' = y + r * sin(cap)\)

    • capOK(int largeur, int hauteur)
      qui retourne un booléen dont la valeur est vraie (\(true\)) si le cap actuel de la tête est tel que le prochain déplacement maintiendra la tête entièrement dans la zone de dessin et faux (\(false\)) sinon. Plus précisément, cette méthode vérifie que le point \((x',y')\) défini par

      \(x' = x + r * cos(cap)\) et \(y' = y +r * sin(cap)\)

      est à une distance \(>= r\) de chacun des bords de la zone de dessin de taille \(largeur\), \(hauteur\).

3.3 La classe Chenille

La classe Chenille définit trois attributs pour une chenille :

  • sa tête,
  • la liste de ses anneaux stockée dans un tableau,
  • la zone de dessin dans laquelle la chenille est affichée.

Elle possède un constructeur permettant, à partir d'une zone de dessin donnée en paramètre, de créer une chenille en position initiale (horizontale avec le centre de la tête placé au centre de la zone de dessin). A la création d'une chenille il est possible de fixer le nombre d'anneaux et le rayon de ceux-ci et de la tête.

Elle possède également 2 méthodes publiques:

  • public void dessiner(Graphics g)
    pour dessiner la chenille,
  • public void deplacer()
    pour faire effectuer à la chenille un déplacement élémentaire selon l'algorithme donné au § 2.2,

4. Travail à effectuer

4.1 implémenter les classes Anneau, Tête et Chenille.

Dans ce premier exercice, il s'agit d'implémenter les classes permettant de modéliser une Chenille et les tester dans une première application d'animation. Pour réaliser cette application, vous vous baserez sur le modèle de l'application AppliVisage vue lors du TP 1. En particulier, vous pourrez réutiliser, moyennant quelques petites adaptations, le code des classes AppliVisage.java et Dessin.java.

1. Créez un répertoire TP07 dans votre répertoire de travail PLAI/Java et dans ce répertoire créez un projet Java (projet maven) de groupId fr.im2ag.m2cci et de nom (artifactId) chenilles (pour avoir les différentes étapes à suivre vous pouvez vous référer au TP précédent : Créer un projet Java avec Maven sous VSCode)

2. Créer un classe Anneau et implémentez là conformément aux spécifications précédentes.

3. Vérifiez la conformité de votre classe Anneau en exécutant les tests unitaires JUnit5 définis dans la classe AnneauTest.

  1. configurez votre projet (fichier pom.xml) afin d'utiliser le framework de tests JUnit5
  2. créez une classe de test AnneauTest
  3. remplacez le code de cette classe par le code qui vous est fourni ici
  4. éxécutez les tests et vérifiez que tous réussissent

4. Créez un classe Tete et implémentez ses constructeurs et sa méthode dessiner() conformément aux spécifications précédentes (les autres méthodes devierCap, deplacerSelonCap() et capOK() seront implémentées ultérieurement).

5. Créez un classe Chenille et implémentez ses constructeurs et sa méthode dessiner() conformément aux spécifications précédentes (la méthode deplacer(), sera implémentées ultérieurement).

6. a) Recopiez dans les sources de votre projet chenille AppliVisage.java et Dessin.java vues lors du TP n°1, (pour télécharger ces classes, effectuez un clic droit, Enregistrer la cible du lien sous... sur les liens précédents)

b) modifiez ces classes de manière à ce que au lieu de manipuler des objets de type VisageRond, elles manipulent des objets de type Chenille.

c) Renommez la classe AppliVisage en AppliChenille.

7. dans AppliChenille, créez une chenille de 10 anneaux de rayon 20 et vérifiez que l'exécution de l'application donne bien l'affichage attendu (chenille avec la tête au centre de l'écran et 10 anneaux alignés horizontalement à gauche de la tête).

Chenille de 10 anneaux de rayon 20 pixels, située en position initiale au centre de la zone de dessin

4.2 Animer la Chenille

Une fois que votre application crée et affiche correctement la chenille dans sa position initiale, vous allez implémenter le code permettant de la déplacer.

  1. Dans la classe Tete implémentez les méthodes devierCap(), deplacerSelonCap() et capOK() conformément aux spécifications fournies.
  2. Dans la classe Chenille implémentez la méthode deplacer() conformément à la spécification fournie.
  3. Exécutez l'application AppliChenille et vérifiez que la Chenille se comporte comme attendu (voir la vidéo ci-dessous)

4.3 Animer \(n\) Chenilles

Modifiez l'application AppliChenille de manière à pouvoir animer au choix une, deux, trois, ... n chenilles, le nombre de chenilles étant passé au programme principal (méthode main) à l'aide du paramètre String[] args qui permet de récupérer les arguments de la ligne de commande utilisée pour lancer la JVM.

indications :

  • Pour convertir l'argument fourni sur la ligne de commande on pourra utiliser la méthode statique parseInt de la classe Integer du package java.lang (voir la javadoc JDK 8, JDK 17 ou JDK 21)).

  • Pour exécuter depuis VSCode en fixant les arguments de la machine virtuelle Java

    figure 4: configuration de l'exécution sous Netbeans.

4.4 Création de nouveaux types de Chenilles

Imaginez de nouveaux types de chenilles. Par exemple des chenilles colorées, des chenilles avec une tête définie par une image comme sur l'exemple ci-dessous.

figure 5: Les chenilles colorées et les chenilles "Star War"

Pour dessiner des chenilles comme ci-dessus, vous pouvez utiliser les images png suivantes

darthVador.png
leila.png
c3po.png
stormTrooper.png

et vous référer à la leçon "Working with Images" du tutorial Java2D.

1. En utilisant au mieux les possibilités que vous offrent l'héritage et le polymorphisme, créez deux nouvelles classes ChenilleCouleur et ChenilleImage qui permettent respectivement d'afficher une chenille avec une couleur donnée et une chenille dont la tête est représentée par une image (carrée, 64x64 pixel pour les images précédentes). Complétez le diagramme de classes UML de la figure 4 pour y faire figurer les nouvelles classes nécessaires.

la figure 6 ci-dessous vous suggère une hiérarchie d'héritage permettant d'atteindre ce but en écrivant un minimum de code.

figure 6: Hiérarchie d'héritage pour définir de nouveaux types de Chenilles

Le constructeur Chenille(Dessin d, int nba, int r, int Tete) permet de factoriser le code créant le corps (tableau d'objets Anneau). Il pourra être invoqué par les autres constructeurs de chenilles (soit par this(...) dans la classe Chenille, soit par super(...) dans les sous-classes.

Par contre le fait qu'il soit protected interdit son utilisation par l'opérateur new en dehors de la classe Chenille ou de l'une des ses sous-classes. Ceci permet de préserver la relation de composition qui lie une chenille à sa tête : le cycle de vie de la tête est lié au cycle de vie de la chenille et seule la chenille a une référence vers sa tête.

2. Implémentez les classes ChenilleCouleur et ChenilleImage et ecrivez une application permettant d'afficher simultanément des chenilles colorées et des chenilles "Star war".