Avec un langage non multitâche, on réalise une animation en créant une
boucle sans fin dans laquelle on met régulièrement à jour l'affichage. Cette
méthode présente l'inconvénient majeur d'occuper 100% du temps du processeur.
JAVA peut gérer plusieurs tâches simultanées : Lorsque la machine virtuelle
exécute une applet classique elle gère en fait plusieurs processus dont l'exécution
du code de l'applet et la gestion dynamique de la mémoire. Pour réaliser une
animation, il faut ajouter à l'applet un processus (Thread) supplémentaire.
Un thread possède son propre flot d'instruction indépendant de celui de l'applet
et dont le déroulement est le suivant. Une méthode start( ) démarre la
méthode run( ) qui contient le code à exécuter par le processus. Une
méthode stop( ) permet de terminer d'exécution de la méthode run( ).
Pendant son exécution un thread peut être endormi pour une certaine durée [sleep(
) ], mis en attente [suspend( )], réactivé [resume( )].
Rappel
: Le navigateur lance la méthode start( ) à chaque fois que le cadre de l'applet
devient visible et la méthode stop( ) quand ce cadre disparaît de l'affichage.
Pour la mise en place d'un thread, on commence par ajouter dans l'entête
de l'applet "implements Runnable" qui signifie que l'on va
implémenter la méthode abstraite run( ) de la classe Runnable (une interface
de java.lang). On crée ensuite comme variable de classe de l'applet une instance
d'un objet Thread. On surcharge les méthodes start( ) et stop( ). Dans la méthode
start( ), on teste l'existence du thread : on le crée s'il n'existe pas puis on
active la méthode start( ) par [nom_thread.start( )] de ce thread [lancement de la méthode run( )]
puis on implémente la méthode run( ).
Pour
un processus très simple, la méthode run( ) peut se résumer à une boucle sans
fin qui appelle successivement la méthode repaint( ) de l'applet puis met le
thread en sommeil. L'appel à la méthode sleep peut générer une exception : il
faut impérativement traiter celle-ci au moyen de la séquence try, catch
prévue pour le traitement des exceptions en JAVA. Par contre le bloc catch peut
être vide.
Dans l'animation proposée en exemple, on se contente de déplacer une ellipse.
import java.applet.*;
import java.awt.*;
public class anima1 extends Applet implements Runnable
{ Thread runner
= null; //variables de classe
double t;
public void init()
{ setBackground
(Color.lightGray);}
public void paint(Graphics
g)
{ int
X=140+(int)(120*Math.sin(t)); //calcul de la position
t+=0.05; //déplacement lors de l'appel suivant
g.fillOval(X,20,50,100);}
public void start()
//surcharge
de la méthode
{ if
(runner == null) //test
d'existence
{ runner
= new Thread(this); //création
, this désigne l'applet
runner.start();}}
// lancement de
la méthode run( )
public void stop()
{ if
(runner != null)
{ runner.stop();
runner
= null;}} //destruction
public void run()
{ while
(true) // boucle sans fin
{ try
//action à réaliser
{ repaint();
//redessiner
Thread.sleep(20);}
//pause de 20 ms
catch
(InterruptedException e)
{ stop();}}}
//traitement (facultatif)
de l'exception
}
J'ai fait volontairement le choix de présenter une animation dont le résultat n'est pas très satisfaisant. L'image scintille et des traces grises traversent régulièrement l'ellipse. Pour comprendre l'origine de ce phénomène, il faut se souvenir que la méthode repaint( ) commence par appeler la méthode update( Graphics g) qui efface le cadre de l'applet. Comme cet effacement qui est à l'origine des défauts constatés la solution est de surcharger la méthode update( ).
Les auteurs de manuels sur JAVA proposent différentes méthodes pour remédier à ces défauts : effacer uniquement les parties mobiles, clipping, double tampon ... Après avoir expérimenté toutes ces techniques, je considère que seule la méthode du double tampon est efficace à 100 % et que les autres sont du bricolage souvent plus difficile à mettre en oeuvre que cette méthode.
Principe : Le principe de cette
technique d'affichage consiste à préparer la nouvelle image à visualiser dans
un tampon (zone de la mémoire) puis à l'afficher à l'écran quand elle est entièrement
terminée.
L'affichage correspond à la copie d'une zone mémoire dans une autre. Tous les
processeurs modernes sont dotés d'instructions spécifiques pour effectuer ces
transferts et leur durée est très brève.
Méthode à utiliser
:
Il faut déclarer une image et le contexte graphique associé.
Ces deux objets sont
ensuite crées dans init( ) car c'est à ce moment seulement que la taille de
la fenêtre de l'applet est connue. Le dessin est réalisé dans le nouveau contexte graphique
par la méthode paint( ). Pour terminer celle-ci, on transfert l'image préparée
dans le contexte graphique de l'applet avec la méthode drawImage( ). Les
arguments de cette méthode sont le nom de la zone mémoire, les coordonnées du
coin supérieur gauche, la couleur du fond et un objet observateur d'image. Comme ici on a la certitude
que la totalité de l'image réside en mémoire, on peut se contenter de celui
de l'applet (utilisation de this). Il faut ensuite surcharger
la méthode update( ).
Si la dimension de la fenêtre de votre navigateur est suffisante, vous pouvez comparer les deux applets.
import java.applet.*;
import
java.awt.*;
public class anima2 extends
Applet implements Runnable
{ private Thread runner = null;
Image
ima; Graphics h; //déclarations
int
la,ha;
double t;
public
void init()
{ setBackground (Color.lightGray);
la=size().width; ha=size().height;
//taille de l'applet
ima=createImage(la,ha);
//creation d'une zone
la*ha pixels
h=ima.getGraphics();}
//contexte
graphique associé à l'image
public
void update(Graphics g) //surcharge
indispensable
{ paint(g);}
public
void paint(Graphics g)
{ h.clearRect(0,0,la,ha);
//effacement de
la zone de dessin
int
X=140+(int)(120*Math.sin(t));
t+=0.05;
h.fillOval(X,20,50,100);
g.drawImage(ima,0,0,Color.white,this);}
//transfert de ima vers
l'écran
//les méthodes start, stop et run sont identiques à celles de l'exemple précédent
public
void destroy() //voir
la remarque ci-dessous
{ h.dispose(); ima.flush();}
}
Cette technique peut également être utilisée (même en dehors des animations) à chaque fois que l'on oblige l'applet à se redessiner avec une fréquence élevée.
Remarques :
1. Le gestionnaire de mémoire (ramasse-miettes) ne gère pas
automatiquement la destruction des objets Images ni des contextes graphiques. L'utilisation de la
méthode destroy( ) permet au programmeur de terminer proprement son programme
en assurant une libération correcte de la mémoire avec les méthodes dispose(
) et flush( ).
2. J'ai suivi l'usage de nommer cette technique
"double tampon" alors que l'on utilise en fait un seul tampon mémoire.
Il est aussi possible de réaliser des animations en utilisant l'affichage
successif d'images. On obtient un effet analogue à celui des images gif animées.
La difficulté de cette méthode provient de ce qu'il faut s'assurer que
les images ont bien été chargées à partir du serveur avant de lancer leur affichage
sur le client.
Dans le listing ci-dessous on retrouve les techniques
examinées plus haut. On pourra noter que les initialisations sont réalisées ici
au début de la méthode run( ) et pas dans une méthode init( ). Le
boolean ok est mis à "true" quand le chargement de la totalité des images
à partir du serveur est terminé. Le contrôle du chargement est effectué par un objet de
la classe MediaTracker du package java.awt. Dans notre exemple, les images sont stockées (sur le serveur) dans le sous-répertoire "image"
du répertoire de la page html de chargement de l'applet. Pour avoir un affichage
correct, il est essentiel que toutes les images aient les mêmes dimensions. Chaque
image recouvrant exactement la précédente, il est en principe inutile d'effacer.
Les
méthodes sleep( ) du Thread et waitForAll du MediaTracker pouvant être
à l'origine d'une exception, il faut utiliser des blocs try et catch.
import java.applet.*;
import
java.awt.*;
public class anima3 extends
Applet implements Runnable
{ Thread runner = null;
Graphics
h; Image imag[];
int index,large
= 0,haut = 0;
boolean ok = false;
final int nbima
= 10;
private void displayImage(Graphics
g)
{ if (!ok) return;
g.drawImage(imag[index],(size().width
- large)/2,(size().height - haut)/2, this);} //centrage de l'affichage
public void paint(Graphics
g)
{ if (ok){ //chargement
terminé
//g.clearRect(0,
0, size().width, size().height); effacement si surcharge de update()
displayImage(g);}
else
g.drawString("Chargement",10,20);}
public void start()
{ if
(runner == null){
runner = new Thread(this);
runner.start();}}
public void stop()
{ if
(runner != null){
runner.stop();
runner
= null;}}
public void run()
{ index
= 0;
if (!ok) // chargement des images
{ repaint();
h
= getGraphics();
imag = new Image[nbima];
//tableau d'images
MediaTracker
tracker = new MediaTracker(this); //
String
strImage;
for (int i = 0; i <
nbima; i++){ //boucle
de chargement
strImage
= "image/ima" +
i + ".gif"; //nom
du fichier
imag[i]
= getImage(getDocumentBase(), strImage); //détermination de son adresse
tracker.addImage(imag[i],
0);} //chargement
try{
tracker.waitForAll();
//attente de réalisation
de la totalité de la boucle
ok
= !tracker.isErrorAny();}
catch (InterruptedException
e){ }
large = imag[0].getWidth(this);
//dimensions des
images
haut
= imag[0].getHeight(this);}
repaint();
while
(true){ //boucle
d'animation
try{
displayImage(h);
index++;
if
(index == nbima) index = 0;
Thread.sleep(100);}
//100 ms entre
chaque image
catch
(InterruptedException e){stop();}}}
public void destroy() //pour quitter proprement
{ h.dispose();
for
(int i = 0; i < nbima; i++) imag[i].flush();}
}
Il est possible de gérer de manière simultanée plusieurs processus qui peuvent être indépendants en étendant la classe Thread. Afin de ne pas trop allonger le listing de l'exemple, les deux processus mis en place utilisent la même classe (process) mais les deux activités sont totalement indépendantes et asynchrones. Dans le premier processus, on déplace un rectangle noir de droite à gauche; dans le second on déplace un rectangle rouge de haut en bas. Dans un cas concret, on pourra bien sur utiliser des classes différentes pouvant agir sur des objets différents.
La méthode run( ) de l'applet crée deux instances p1 et p2 de la classe process puis lance leur exécution avec p1.start( ) et p2.start( ). La classe process modifie dans sa méthode run( ) la position de l'origine des rectangles qui lui sont passés en argument. Cette méthode exécute ensuite une boucle infinie qui redessine l'applet. La méthode sleep( ) d'un Thread pouvant être à l'origine d'une exception, il faut utiliser des blocs try et catch. L'usage d'un double tampon permettrait d'améliorer cette animation.
import java.applet.*;
import
java.awt.*;
class process extends Thread
{ Rectangle
rp; //objet à modifier
boolean ver;
process(Rectangle r,boolean
v) //constructeur
{ rp=r; ver=v;}
public void run()
{ int
del =(ver) ? 20 : 40; //pour
avoir des processus asynchrones
while(true){
for
(int i=0; i<100; i++){
if (ver) rp.x=2*i;
else rp.y=i; //déplacement
dans un sens
try{Thread.sleep(del);}
catch
(InterruptedException e){ }}
for (int i=100; i>0;
i--){ //puis
dans l'autre
if (ver) rp.x=2*i; else rp.y=i;
try{Thread.sleep(del);}
catch
(InterruptedException e){ }}}}}
public class anima4 extends
Applet implements Runnable
{ Thread runner = null;
Rectangle
r=new Rectangle(0,0,40,20);
Rectangle r2=new Rectangle(0,0,20,40);
process
p1,p2;
public void
init()
{ setBackground(Color.lightGray);}
public void paint(Graphics
g)
{ g.setColor(Color.black);
g.fillRect(10+r.x,60+r.y,r.width,r.height);
g.setColor(Color.red);
g.fillRect(100+r2.x,10+r2.y,r2.width,r2.height);}
public void start()
{ if
(runner == null)
{ runner = new Thread(this);
runner.start();}}
public void stop()
{ if
(runner != null)
{ runner.stop();
runner
= null;}}
public void run()
{ p1=new
process(r,true);
p2=new process(r2,false);
p1.start();
p2.start();
while
(true){
try{
repaint();
Thread.sleep(10);}
catch
(InterruptedException e){ }}}
}