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. Utiliser de préférence une construction de la forme class Balle extends Thread.

Rappels sur 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 grille contient un état booléen concernant l'occupation de la zone qui peut être accédé par les méthodes suivantes :

Modifier Balle.avance() pour gérer l'entrée et la sortie dans la zone. La partie déplacement deviendra ainsi :

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) { ... }.

Rappels sur synchronized :

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.

Rappels sur wait() et notify() :

Une solution.

Exercice 2. Pause et raquette

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.

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

Exercice 2.2 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.3 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.4 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().

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.

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

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: Mon Apr 25 10:00:53 CEST 2005