Le fil rouge : une animation

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Dans ce chapitre, nous allons voir comment créer une animation simple. Il ne vous sera pas possible de réaliser un jeu au terme de ce chapitre, mais je pense que vous y trouverez de quoi vous amuser un peu.

Nous réutiliserons cette animation dans plusieurs chapitres de cette troisième partie afin d'illustrer le fonctionnement de divers composants graphiques. L'exemple est rudimentaire, mais il a l'avantage d'être efficace et de favoriser votre apprentissage de la programmation événementielle.

Je sens que vous êtes impatients de commencer alors… allons-y !

Création de l'animation

Voici un résumé de ce que nous avons déjà codé :

  • une classe héritée de JFrame ;
  • une classe héritée de JPanel avec laquelle nous faisons de jolis dessins. Un rond, en l'occurrence.

En utilisant ces deux classes, nous allons pouvoir créer un effet de déplacement. Vous avez bien lu : j'ai parlé d'un effet de déplacement ! Le principe réside dans le fait que vous allez modifier les coordonnées de votre rond et forcer votre objet Panneau à se redessiner. Tout cela - vous l'avez déjà deviné - dans une boucle.

Jusqu'à présent, nous avons utilisé des valeurs fixes pour les coordonnées du rond, mais il va falloir dynamiser tout ça. Nous allons donc créer deux variables privées de type int dans la classe Panneau : appelons-les posX et posY. Dans l'animation sur laquelle nous allons travailler, notre rond viendra de l'extérieur de la fenêtre. Partons du principe que celui-ci a un diamètre de cinquante pixels : il faut donc que notre panneau peigne ce rond en dehors de sa zone d'affichage. Nous initialiserons donc nos deux variables d'instance à « -50 ». Voici le code de notre classe Panneau :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;

public class Panneau extends JPanel {
  private int posX = -50;
  private int posY = -50;

  public void paintComponent(Graphics g){
    g.setColor(Color.red);
    g.fillOval(posX, posY, 50, 50);
  }

  public int getPosX() {
    return posX;
  }

  public void setPosX(int posX) {
    this.posX = posX;
  }

  public int getPosY() {
    return posY;
  }

  public void setPosY(int posY) {
    this.posY = posY;
  }        
}

Il ne nous reste plus qu'à faire en sorte que notre rond se déplace. Nous allons devoir trouver un moyen de changer ses coordonnées grâce à une boucle. Afin de gérer tout cela, ajoutons une méthode privée dans notre classe Fenetre que nous appellerons en dernier lieu dans notre constructeur. Voici donc ce à quoi ressemble notre classe Fenetre :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.awt.Dimension; 
import javax.swing.JFrame;

public class Fenetre extends JFrame{
  private Panneau pan = new Panneau();

  public Fenetre(){        
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    this.setContentPane(pan);
    this.setVisible(true);
    go();
  }

  private void go(){
    for(int i = -50; i < pan.getWidth(); i++){
      int x = pan.getPosX(), y = pan.getPosY();
      x++;
      y++;
      pan.setPosX(x);
      pan.setPosY(y);
      pan.repaint();  
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }       
}

Vous vous demandez sûrement l'utilité des deux instructions à la fin de la méthode go(). La première de ces deux nouvelles instructions est pan.repaint(). Elle demande à votre composant, ici un JPanel, de se redessiner.

La toute première fois, dans le constructeur de notre classe Fenetre, votre composant avait invoqué la méthode paintComponent() et avait dessiné un rond aux coordonnées que vous lui aviez spécifiées. La méthode repaint() ne fait rien d'autre qu'appeler à nouveau la méthode paintComponent() ; mais puisque nous avons changé les coordonnées du rond par le biais des accesseurs, la position de celui-ci sera modifiée à chaque tour de boucle.

La deuxième instruction, Thread.sleep(), est un moyen de suspendre votre code. Elle met en attente votre programme pendant un laps de temps défini dans la méthode sleep() exprimé en millièmes de seconde (plus le temps d'attente est court, plus l'animation est rapide). Thread est en fait un objet qui permet de créer un nouveau processus dans un programme ou de gérer le processus principal.

Dans tous les programmes, il y a au moins un processus : celui qui est en cours d'exécution. Vous verrez plus tard qu'il est possible de diviser certaines tâches en plusieurs processus afin de ne pas perdre du temps et des performances. Pour le moment, sachez que vous pouvez effectuer des pauses dans vos programmes grâce à cette instruction :

1
2
3
4
5
try{
  Thread.sleep(1000); //Ici, une pause d'une seconde
}catch(InterruptedException e) {
  e.printStackTrace();
}

Cette instruction est dite « à risque », vous devez donc l'entourer d'un bloc try{…}catch{…} afin de capturer les exceptions potentielles. Sinon, erreur de compilation !

Maintenant que la lumière est faite sur cette affaire, exécutez ce code, vous obtenez la figure suivante.

Rendu final de l'animation

Bien sûr, cette image est le résultat final : vous devez avoir vu votre rond bouger. Sauf qu'il a laissé une traînée derrière lui… L'explication de ce phénomène est simple : vous avez demandé à votre objet Panneau de se redessiner, mais il a également affiché les précédents passages de votre rond ! Pour résoudre ce problème, il faut effacer ces derniers avant de redessiner le rond.

Comment ? Dessinez un rectangle de n'importe quelle couleur occupant toute la surface disponible avant de peindre votre rond. Voici le nouveau code de la classe Panneau :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;

public class Panneau extends JPanel {
  private int posX = -50;
  private int posY = -50;

  public void paintComponent(Graphics g){
    //On choisit une couleur de fond pour le rectangle
    g.setColor(Color.white);
    //On le dessine de sorte qu'il occupe toute la surface
    g.fillRect(0, 0, this.getWidth(), this.getHeight());
    //On redéfinit une couleur pour le rond
    g.setColor(Color.red);
    //On le dessine aux coordonnées souhaitées
    g.fillOval(posX, posY, 50, 50);
  }

  public int getPosX() {
    return posX;
  }

  public void setPosX(int posX) {
    this.posX = posX;
  }

  public int getPosY() {
    return posY;
  }

  public void setPosY(int posY) {
    this.posY = posY;
  }
}

la figure suivante représente l'animation à différents moments.

Capture de l'animation à trois moments différents

Cela vous plairait-il que votre animation se poursuive tant que vous ne fermez pas la fenêtre ? Oui ? Alors, continuons.

Améliorations

Voici l'un des moments délicats que j'attendais. Si vous vous rappelez bien ce que je vous ai expliqué sur le fonctionnement des boucles, vous vous souvenez de mon avertissement à propos des boucles infinies. Eh bien, ce que nous allons faire ici, c'est justement utiliser une boucle infinie.

Il existe plusieurs manières de réaliser une boucle infinie : vous avez le choix entre une boucle for, while ou do… while. Regardez ces déclarations :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//Exemple avec une boucle while
while(true){
  //Ce code se répétera à l'infini, car la condition est toujours vraie !
}

//Exemple avec une boucle for
for(;;)
{
  //Idem que précédemment : il n'y a pas d'incrémentation donc la boucle ne se terminera jamais.
}

//Exemple avec do… while
do{
  //Encore une boucle que ne se terminera pas.
}while(true);

Nous allons donc remplacer notre boucle finie par une boucle infinie dans la méthode go() de l'objet Fenetre. Cela donne :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private void go(){
  for(;;){
    int x = pan.getPosX(), y = pan.getPosY();
    x++;
    y++;
    pan.setPosX(x);
    pan.setPosY(y);
    pan.repaint();  
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Si vous avez exécuté cette nouvelle version, vous vous êtes rendu compte qu'il reste un problème à régler… En effet, notre rond ne se replace pas à son point de départ une fois qu'il a atteint l'autre côté de la fenêtre !

Si vous ajoutez une instruction System.out.println() dans la méthode paintComponent() inscrivant les coordonnées du rond, vous verrez que celles-ci ne cessent de croître.

Le premier objectif est bien atteint, mais il nous reste à résoudre ce dernier problème. Pour cela, il faut réinitialiser les coordonnées du rond lorsqu'elles atteignent le bout de notre composant. Voici donc notre méthode go() revue et corrigée :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private void go(){
  for(;;){
    int x = pan.getPosX(), y = pan.getPosY();
    x++;
    y++;
    pan.setPosX(x);
    pan.setPosY(y);
    pan.repaint();  
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    //Si nos coordonnées arrivent au bord de notre composant
    //On réinitialise
    if(x == pan.getWidth() || y == pan.getHeight()){
      pan.setPosX(-50);
      pan.setPosY(-50);
    }
  }
}

Ce code fonctionne parfaitement (en tout cas, comme nous l'avons prévu), mais avant de passer au chapitre suivant, nous pouvons encore l'améliorer. Nous allons maintenant rendre notre rond capable de détecter les bords de notre Panneau et de ricocher sur ces derniers !

Jusqu'à présent, nous n'attachions aucune importance au bord que notre rond dépassait. Cela est terminé ! Dorénavant, nous séparerons le dépassement des coordonnées posX et posY de notre Panneau.

Pour les instructions qui vont suivre, gardez en mémoire que les coordonnées du rond correspondent en réalité aux coordonnées du coin supérieur gauche du carré entourant le rond.

Voici la marche à suivre :

  • si la valeur de la coordonnée x du rond est inférieure à la largeur du composant et que le rond avance, on continue d'avancer ;
  • sinon, on recule.

Nous allons faire de même pour la coordonnée y.

Comment savoir si l'on doit avancer ou reculer ? Grâce à un booléen, par exemple. Au tout début de notre application, deux booléens seront initialisés à false, et si la coordonnée x est supérieure à la largeur du Panneau, on recule ; sinon, on avance. Idem pour la coordonnée y.

Dans ce code, j'utilise deux variables de type int pour éviter de rappeler les méthodes getPosX() et getPosY().

Voici donc le nouveau code de la méthode go() :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
private void go(){
  //Les coordonnées de départ de notre rond
  int x = pan.getPosX(), y = pan.getPosY();
  //Le booléen pour savoir si l'on recule ou non sur l'axe x
  boolean backX = false;
  //Le booléen pour savoir si l'on recule ou non sur l'axe y
  boolean backY = false;

  //Dans cet exemple, j'utilise une boucle while
  //Vous verrez qu'elle fonctionne très bien
  while(true){
    //Si la coordonnée x est inférieure à 1, on avance
    if(x < 1)
      backX = false;

    //Si la coordonnée x est supérieure à la taille du Panneau moins la taille du rond, on recule
    if(x > pan.getWidth()-50)
      backX = true;

    //Idem pour l'axe y
    if(y < 1)
      backY = false;
    if(y > pan.getHeight()-50)
      backY = true;

    //Si on avance, on incrémente la coordonnée
    //backX est un booléen, donc !backX revient à écrire
    //if (backX == false)
    if(!backX)
      pan.setPosX(++x);

    //Sinon, on décrémente
    else
      pan.setPosX(--x);

    //Idem pour l'axe Y
    if(!backY)
      pan.setPosY(++y);
    else
      pan.setPosY(--y);

    //On redessine notre Panneau
    pan.repaint();

    //Comme on dit : la pause s'impose ! Ici, trois millièmes de seconde
    try {
      Thread.sleep(3);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Exécutez l'application : le rond ricoche contre les bords du Panneau. Vous pouvez même étirer la fenêtre ou la réduire, ça marchera toujours ! On commence à faire des choses sympa, non ?

Voici le code complet du projet si vous le souhaitez :

Classe Panneau

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;

public class Panneau extends JPanel {

  private int posX = -50;
  private int posY = -50;

  public void paintComponent(Graphics g) {
    // On décide d'une couleur de fond pour notre rectangle
    g.setColor(Color.white);
    // On dessine celui-ci afin qu'il prenne tout la surface
    g.fillRect(0, 0, this.getWidth(), this.getHeight());
    // On redéfinit une couleur pour notre rond
    g.setColor(Color.red);
    // On le dessine aux coordonnées souhaitées
    g.fillOval(posX, posY, 50, 50);
  }

  public int getPosX() {
    return posX;
  }

  public void setPosX(int posX) {
    this.posX = posX;
  }

  public int getPosY() {
    return posY;
  }

  public void setPosY(int posY) {
    this.posY = posY;
  }
}

Classe Fenetre

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.awt.Dimension;
import javax.swing.JFrame;

public class Fenetre extends JFrame {

  public static void main(String[] args) {
    new Fenetre();
  }

  private Panneau pan = new Panneau();

  public Fenetre() {
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    this.setContentPane(pan);
    this.setVisible(true);

    go();
  }

  private void go() {
    // Les coordonnées de départ de notre rond
    int x = pan.getPosX(), y = pan.getPosY();
    // Le booléen pour savoir si l'on recule ou non sur l'axe x
    boolean backX = false;
    // Le booléen pour savoir si l'on recule ou non sur l'axe y
    boolean backY = false;

    // Dans cet exemple, j'utilise une boucle while
    // Vous verrez qu'elle fonctionne très bien
    while (true) {
      // Si la coordonnée x est inférieure à 1, on avance
      if (x < 1)
        backX = false;
      // Si la coordonnée x est supérieure à la taille du Panneau moins la taille du rond, on recule
      if (x > pan.getWidth() - 50)
        backX = true;
      // Idem pour l'axe y
      if (y < 1)
        backY = false;
      if (y > pan.getHeight() - 50)
        backY = true;

      // Si on avance, on incrémente la coordonnée
      // backX est un booléen, donc !backX revient à écrire
      // if (backX == false)
      if (!backX)
        pan.setPosX(++x);
      // Sinon, on décrémente
      else
        pan.setPosX(--x);
      // Idem pour l'axe Y
      if (!backY)
        pan.setPosY(++y);
      else
        pan.setPosY(--y);

      // On redessine notre Panneau
      pan.repaint();
      // Comme on dit : la pause s'impose ! Ici, trois millièmes de seconde
      try {
        Thread.sleep(3);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

  • À l'instanciation d'un composant, la méthode paintComponent() est automatiquement appelée.
  • Vous pouvez forcer un composant à se redessiner en invoquant la méthode repaint().
  • Pensez bien à ce que va produire votre composant une fois redessiné.
  • Pour éviter que votre animation ne bave, réinitialisez le fond du composant.
  • Tous les composants fonctionnent de la même manière.
  • L'instruction Thread.sleep() permet d'effectuer une pause dans le programme.
  • Cette méthode prend en paramètre un entier qui correspond à une valeur temporelle exprimée en millièmes de seconde.
  • Vous pouvez utiliser des boucles infinies pour créer des animations.