Ce TD propose d'aborder certains aspects de la programmation concurrente par un jeu inspiré du casse-brique dans lequel des balles se déplacent dans une grille. La première partie est particulièrement axée sur les problèmes de synchronisation alors que la seconde développe l'élaboration du jeu.
Il est important de traiter les l'exercices 1 et 2. L'exercice 3 est facultatif.
Avant de partir, déposez vos programmes en tapant dans une fenêtre xterm la commande :
/users/profs/info/Depot/INF_431/deposer [pgms] TD_11 [gpe]
où [pgm] doit être remplacé par la liste des noms des fichiers contenant vos programmes et [gpe] doit être remplacé par votre numéro de groupe.
Le programme Brique.java
fournit une base de départ pour animer une balle dans
une grille de cases. Le programme contient principalement
quatre classes. Seule la classe Balle
est
à modifier dans l'exercice 1.
Brique
construit une fenêtre
contenant une grille et des balles. Testez par exemple le programme
par java Brique 30 33 1 qui construit une
grille 30x33 et lance 1 balle.
Balle
permet de définir une balle.
Chaque balle a un numéro et une position dans la grille.
Une balle se déplace toujours selon une diagonale et rebondit
sur les bords.
Grille
contient principalement
un tableau tab
de cases. Une partie de la grille
apparaît en grisé et définit une zone grisée
(utilisée pour
l'exercice 1.2). Étant donné une Grille g
,
g.tab[x][y]
désigne la case de coordonnées x, y
.
Case
ainsi que ses classes dérivées
CaseBord
et CaseZone
définit les
cases et leur méthodes d'affichage : les bords en noir,
la zone en grisé. Si la case est occupée par une balle, celle-ci
est représentée par une couleur associée à son numéro.
Étant donné une Case c
,
c.bord ()
renvoie true
si c
est sur un bord.
c.zone ()
renvoie true
si c
est dans la zone grisée.
c.occupe (num)
permet d'indiquer que
c
est occupée par la balle de numéro num
.
c.libere (num)
permet d'indiquer que
c
n'est plus occupée par la balle de numéro num
.
Tester ce programme avec java Brique 30 33
15. La méthode statique Balle.lancerDesBalles()
est
appelée pour lancer plusieurs balles. Que se passe-t'il ?
Modifier Balle.lancerDesBalles()
pour que
plusieurs balles soient lancées de manière concurrente et non séquentielle. Pour cela,
modifier la classe Balle
pour lancer un thread par
balle. Utiliser de préférence une construction de la forme
class Balle extends Thread
.
Thread
comme
class T extends Thread
permet de créer un nouveau
thread pour chaque nouvel objet de la classe.
start ()
permet de lancer le thread :
T t = new T () ; t.start () ;
run()
de l'objet
est appelée. Par défaut, cette méthode ne fait rien, il faut donc
la redéfinir dans T
, par exemple :
class T extends Thread { public void run () { System.out.println (Thread.currentThread ()) ; } }
Pour compliquer un peu le jeu, une seule balle doit pouvoir
pénétrer dans la zone grisée à la fois. Pour cela, une Grille grille
contient un état booléen concernant l'occupation de la zone
qui peut être accédé par les méthodes suivantes :
grille.zoneEstOccupee ()
renvoie true
si
la zone est marquée occupée.
grille.occupeZone ()
indique de marquer la zone comme occupée.
(Un message d'erreur s'affiche si une des cases de la zone
s'avère être déjà occupée par une balle.)
grille.libereZone ()
indique de marquer la zone
comme non occupée.
Modifier Balle.avance()
pour gérer l'entrée et
la sortie dans la zone. La partie déplacement deviendra ainsi :
(! grille.tab[x][y].zone ()) && grille.tab[nx][ny].zone()
), elle doit attendre tant que la zone est occupée.
Pour éviter une attente active, on pourra pour l'instant
utiliser un appel à Thread.yield () ;
.
Dès que la zone se libère, elle doit la marquer comme occupée
(par un appel à grille.occupeZone()
).
Balle.avance()
:grille.tab[nx][ny].occupe (num) ;
grille.tab[x][y].libere (num) ;
grille.libereZone ()
).
La principale difficulté de l'exercice consiste à gérer les conflits d'accès entre les balles pour marquer la zone comme occupée. Quand une balle sort de la zone, toutes celles qui sont restées bloquées en attendant vont tenter de marquer la zone comme occupée. Une seule doit réussir et pénétrer dans la zone. Testez votre programme et vérifier que plusieurs balles ne pénètrent pas en même temps dans la zone. (Aucun message d'erreur Probleme de reservation.... ne doit apparaître en sortie du programme.)
Pour éviter que plusieurs balles n'accèdent simultanément
à l'état booléen de la grille, on
utilisera le verrou de l'objet
grille
au moyen de la construction synchronized (grille)
{ ... }
.
o
sans qu'un autre thread vienne changer l'état de o
entre
temps, on peut utiliser une section critique de code
par synchronized (o) { ... }
. Cela revient à prendre
un verrou sur o
avant d'effectuer la section critique
et à le relâcher après. Un autre thread qui veut prendre le verrou
sur o
se trouve bloqué jusqu'à ce que le verrou soit relâché.
o
.
synchronized
indique de prendre le verrou sur
this
. Ainsi,
synchronized void f () { ... }
équivaut à
void f() { synchronized (this) { ... }}
.
Pour compliquer encore les choses, nous allons maintenant utiliser
grille.libereZoneVerif (num)
au lieu
de grille.libereZone()
. Cette fonction plus lente
vérifie de plus qu'aucune balle n'a pénétré dans la zone pendant qu'on
libère la zone. (Elle prend en argument le numéro de la balle qui
libère la zone.)
Bien sûr, il faut maintenant utiliser une section critique
pour l'appel à grille.libereZoneVerif (num)
.
Cependant, on peut arriver à une situation de blocage si une
balle qui sort de la zone n'arrive pas à obtenir le verrou afin de
libérer la zone. Pour cela, un thread qui obtient le verrou
mais ne peut rien en faire (par exemple une balle qui
veut entrer dans la zone alors que celle-ci est occupée)
doit redonner le verrou avec grille.wait()
, ce qui le
met en veille jusqu'à ce qu'un autre thread le
réveille au moyen de grille.notify()
.
On veillera à ce que toute les balles ne viennent pas se bloquer sur
la zone.
o.wait ()
relâche le verrou sur o
et met le thread appelant en veille jusqu'à obtenir
à nouveau le verrou.InterruptedException
(voir l'exercice 2.1)
et doit être appelé par la construction
try { o.wait () ; } catch (InterruptedException e) {}
.
o.notifyAll ()
.
On peut aussi en réveiller un seul (choisi arbitrairement)
par o.notify ()
.
o.wait()
ou o.notify()
, il ne
peuvent donc apparaître qu'à l'intérieur d'une
section critique synchronized (o) { ... }
.
Cet exercice vise à rajouter une raquette pilotée par la souris. Cependant pour lancer le jeu sereinement, nous allons tout d'abord introduire un mécanisme permettant de mettre les balles en pause.
Mettre le jeu en pause lorsque l'on clique à la souris. Un nouveau clic relancera le jeu. Au départ du jeu, les balles seront toutes en pause.
Pour récupérer les évènements de souris, on fera appel à
addMouseListener()
dans le constructeur de Grille
en utilisant
une classe dérivée de MouseAdapter
, on pourra partir de :
class Souris extends MouseAdapter { Souris (Grille g) { grille = g ; } public void mousePressed (MouseEvent e) { System.out.println ("La souris est cliquée.") ; } }
Pour interrompre le cours normal de l'exécution des balles, on
utilisera la méthode
interrupt() héritée de la classe Thread
.
Cette appel permet de provoquer une exception InterruptedException
dans l'exécution du thread sur lequel elle est appelée.
Un champ enPause
dans Balle
permettra de plus d'indiquer que la balle
est en pause. Une fois en pause, c'est-à-dire quand on reçoit
l'exception InterruptedException
dans le code d'exécution
de la balle, il suffit de geler la balle par
if (enPause) wait () ;
jusqu'à ce
qu'un notify()
libère la balle de sa pause.
Rajouter dans Grille
un champ
raquetteX
pour stocker la position de la raquette
et une constante LARG_RAQUETTE = 5
pour stocker
la largeur de la raquette.
Pour introduire la raquette, définir toutes les cases du bas
(y = grille.hauteur - 1
) comme des cases
de type CaseBordRaquette
héritant de CaseBord
.
Une telle case s'affichera en noir si sa position est comprise
entre raquetteX
et raquetteX + LARG_RAQUETTE
,
et en blanc sinon.
Définir une classe RaquetteSouris
étendant
MouseMotionAdapter pour mettre à jour le champ
raquetteX
de la grille. Il suffira alors
d'appeler addMouseMotionListener () avec une
nouvelle RaquetteSouris
pour voir la raquette bouger
avec la souris. On n'oubliera pas de faire des appels aux méthodes
paint()
appropriées pour que les déplacements de
raquette soient effectivement affichés.
Seules les cases raquette de position comprise
entre raquetteX
et raquetteX + LARG_RAQUETTE
réagissent comme un bord.
Une balle qui sort du jeu en passant à côté de la raquette
doit terminer. (Le thread associé doit finir.) On pourra, par
exemple, utiliser
un champ sortie
dans Balle
qui
indique si la balle est sortie du jeu.
Quand toutes les balles sont sorties du jeu, votre programme
doit terminer par un appel à System.exit(0)
.
Pour cela, on attendra que tous les threads des balles aient terminés
grâce à des appels à join().
Un notifyAll()
est maintenant nécessaire pour réveiller
les balles de leur pause car le thread principal
par ses join()
est lui aussi en attente de verrou
sur les balles.
Rajouter des cases briques qui se désintègrent quand elles sont frappées par une balle. Trouver une manière simple de gérer les collisions entre balles. On pourra utiliser la zone grisée pour sérialiser un peu les arrivées de balles du côté de la raquette.
Une solution. qui contient les informations pour sauver des copies d'écran ce qui permet de faire ce ce gif animé.
L'appliquette au début du sujet.
Laissez libre cours à votre imagination : des briques tueuses de balles. Des briques indestructibles, des balles coincées entre des briques qui peuvent être libérées,.... Le jeu peut-être une succession de tableaux, quand toutes les briques d'un tableau sont détruites, on passe au suivant...