TD 11    Casse-brique et programmation concurrente

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]

[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.

Exercice 1. Plusieurs balles

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.

Exercice 1.1 Un thread par balle

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. On pourra s'inspirer d'un exemple du cours ou de la documentation java de la classe Thread.

Exercice 1.2 Synchronisation dans la zone

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 g contient un état booléen concernant l'occupation de la zone :

Modifier Balle.avance() pour gérer l'entrée et la sortie dans la zone. La partie déplacement qui prend effet par les appels à grille.tab[nx][ny].occupe (num) et grille.tab[x][y].libere (num) doit ainsi être complétée :

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.) De même, lorsqu'une balle quitte la zone, aucune balle ne doit pénétrer avant la fin de l'appel à grille.libereZone().

Pour éviter que plusieurs balles n'accèdent simultanément à l'état booléen de la grille, on utilisera un verrou sur la grille au moyen de la construction synchronized (grille) { ... }.

Rappels sur synchronized :

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 wait(), ce qui le met en veille jusqu'à ce qu'un autre thread le réveille au moyen de notify().

Rappels sur wait() et notify() :

On veillera à ce qu'aucune balle ne reste bloquée ou ne ralentit trop le jeu par une attente active.

Une solution.

Exercice 2. Une raquette

Cet exercice vise à rajouter une raquette pilotée par la souris. Les exercices 2.1 et 2.2 concernent la partie interface graphique, tandis que les exercices 2.3 et 2.4 concernent l'interruption des balles. En cas de manque de temps, on pourra utiliser IndiceAwt.java qui fournit les parties awt nécessaires aux exercices 2.1 et 2.2.

Exercice 2.1 Les cases raquette

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.

Exercice 2.2 Attacher la raquette à la souris

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.

Exercice 2.3 Sortie de balle

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().

Exercice 2.4 Pause

Mettre le jeu en pause lorsque l'on appuie sur la barre espace. Un nouvel appui sur celle-ci relancera le jeu. On utilisera addKeyListener() pour intercepter les évènements d'appui de barre espace. (Il suffit de cliquer une fois sur la fenêtre pour qu'elle obtienne le "focus" et reçoive les évènements provenant du clavier.)

Pour interrompre le cours normal de l'exécution des balles, on utilisera interrupt(). Un champ enPause dans Balle permettra de plus d'indiquer que la balle est en pause. Une fois en pause, il suffit de geler la balle par if (enPause) wait () ; jusqu'à ce qu'un notifyAll() libère la balle de sa pause. (Un notify() ne suffirait pas ici car le thread principal par ses join() est lui aussi en attente de verrou sur les balles.)

Au départ du jeu, les balles seront toutes en pause. Il faut que la fenêtre obtienne le "focus" pour qu'elle reçoive les évènements provenant du clavier. Pour cela, on peut aussi faire appel à grille.requestFocus() quand la souris entre dans la fenêtre. Ainsi, on pourra faire en sorte qu'un clic de souris, ou simplement l'entrée de la souris dans la fenêtre, lance le jeu sans avoir besoin de presser de plus la barre espace (utiliser addMouseListener()).

Une solution.

Exercice 3. Casser des briques

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.

Pour aller plus loin

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...


Sujet proposé par Laurent Viennot Last modified: Tue Apr 19 21:41:20 CEST 2005