Date de première publication : 2019/02/27
Notions : Java FX, patron MVC, patron observateur
Nous voulons réaliser un démineur en Java avec Java FX (Avec la bibliothèque Swing, c'est ici !
Le principe du jeu est très simple : on propose à une personne de découvrir petit à petit une zone "minée" en signalant à chaque case, le nombre de mines adjacentes. Il faut découvrir toute la zone sans tomber sur une mine. Pour une plus grande sécurité, on peut en général marquer la zone où l'on est persuadé qu'il y a une mine pour ne pas la découvrir.
Nous voulons programmer ce petit jeu en utilisant le patron de conception Modèle-Vue-Contrôleur, même si en JavaFX on fusionne assez facilement les parties
Vue et Contrôleur. Nous montrerons comment des objets peuvent communiquer entre eux : par appel de méthodes bien entendu mais surtout
en utilisant le patron de conception Observateur/Observable
qui permet à un objet d'être prévenu d'un changement. Le patron Observateur/Observable
permet de mettre en place un couplage "lâche" entre deux classes dans le sens où l'objet observé n'a pas d'information sur l'observateur.
Le modèle
Une pratique usuelle de développement consiste à mettre toutes les classes d'un modèle dans un même package
Nous allons tout d'abord nous intéresser à la zone de jeu, le Terrain
: celui-ci comporte une grille de cellules (voir plus bas)
d'une certaine dimension (nous l'avons choisie carrée :-)) et un nombre de mines à découvrir. Nous le dotons également du nombre de cases
qui n'ont pas encore été ou découvertes ou marquées ainsi que d'un état : partie en cours, partie gagnée, partie perdue.
Pour l'état de la partie, vous pouvez utiliser une énumération : dans son expression la plus simple, elle s'écrit comme en C++ même si en java la notion d'énumération est beaucoup plus puissante (c'est une classe avec constructeur et méthodes)
enum Enumeration { VALEUR1, VALEUR2, VALEUR3 }
Si vous devez utiliser une énumération dans un switch, le type de l'énumération est déduit au niveau du case : seule la valeur suffit.
Cellule
Le terrain est un tableau de cases, que l'on appelera "cellules" (vous avez déjà essayé d'appeler une variable case) pour lequelles on a les informations suivantes :
- la position
x
ety
dans la grille - un entier
valeur
: -1 si c'est une mine ou le nombre de mines adjacentes - un booléen
visible
si la case affiche sa valeur (on dira qu'elle est découverte) - un booléen
selected
si la case est marquée (une case marquée ne peut être découverte)
Écrire un constructeur qui permet d'initialiser les attributs x
, y
et valeur
(visible
et selected
sont "faut par défaux" ;-))
Doter tous les attributs de méthodes get (cela peut se faire automatiquement avec un EDI)
Dois-je vous rappeler qu'avec les conventions d'écriture l'accesseur pour selected
se nomme isSelected()
?
Doter la classe d'autres méthodes : uncover()
qui permet de rendre visible la cellule si elle n'est pas marquée, et toggleSelected()
qui permet de changer la valeur du marquage.
Il faut encore les méthodes set pour les attributs selected
et visible
mais elles nécessitent un peu de travail.
Elles ne devront pas être accessibles de l'extérieur alors je propose de les mettre privées et ces méthodes auront la "lourde" tâche de prévenir d'éventuels
observateurs que l'état de la classe a changé ! Pour réaliser cela, la classe devra être une instance de java.util.Observable
et toute classe qui veut être notifiée
des changements devra implémenter java.util.Observer
.
setChanged();
notifyObservers();
L'observateur devra implémenter une méthode update()
que je vous laisse découvrir dans la documentation.
Les classes Observer
et Observable
sont antérieures à Swing et encore plus à JavaFX (qui implémentent toutes deux ce patron avec des Property. Je garde ce patron car cela permet de s'abstraire de la couche graphique !!!
Si vous ne voulez pas utiliser le patron observateur, il reste quand meme des solutions :
- Passer une référence du terrain à chaque cellule
- Faire un singleton avec le terrain
- Un
java.beans.PropertyChangeListener
depuis la version 9 de Java oùObservable/Observer
a été rendu obsolète.
Terrain
Écrire le constructeur de la classe qui initialisera la grille (taille et nombre de mines donnés en paramètres).
Proposer une méthode create()
qui s'assure que la grille est vierge de toute information, qui génère le nombre fixé de mines
et qui calcule la matrice d'adjacence. Il est alors possible de commencer à jouer ... Cette méthode est appelable plusieurs fois lors d'une même exécution du programme
Le terrain sera un observateur pour chacune des cellules de la grille et cela permettra de prendre en compte les situations suivantes :
- Si une cellule est changée, l'état de la partie de jeu peut être changé.
- On peut aussi programmer la découverte automatique des cellules.
La vue
La vue en Java FX est relativement simple : une Application
et une fenêtre principale à base de Canvas
. Pour avoir un canvas redimensionnable, n'hésitez pas à (re)lire le TP Picasso.
La case graphique
Le canvas permet d'afficher les cellules qui ont différentes représentations en fonction des choix du joueur :
- une zone uniforme si la case n'est pas découverte
- un symbole (une bombe par exemple) représentant une case marquée. On pourra commencer par un cercle!
- un nombre si la case est découverte et qu'elle n'est pas minée.
Voici un morceau de code pour afficher du texte avec un Canvas
gc.setFont(Font.font("Courier New", FontWeight.BOLD, taille));
gc.setTextAlign(TextAlignment.CENTER);
gc.setTextBaseline(VPos.CENTER);
fillText(texte, x, y);
Il faudra implémenter une gestion d'événement souris (MouseEvent
) pour le clic l'interface. Le paramètre de la méthode setOnMousePressed()
contient l'information du bouton appuyé.
Mesurer le temps qui s'écoule...
Je vous propose d'afficher le temps qui s'écoule dans une barre des tâches constituée d'un Label
spécialisé.
Timer timer = new Timer();
timer.schedule(tt, 1000, 1000);
Un Timer
est annulable (méthode cancel()
). TimerTask
est une simple interface qui définit une méthode run()
.
Platform.runLater()
peut vous sauver la mise.
Fin du jeu
Il reste à signaler la fin de la partie au joueur : gagné ou perdu ! Je vais vous proposer trois solutions, elles considèrent que la vue principale observe le terrain afin de connaître le moindre changement de statut dans la partie du jeu.
- La première solution consiste à afficher un simple message avec une
Alert
- La deuxième demande à modifier l'affichage du
Canvas
pour afficher le message "Gagné" ou "Perdu". La barre des tâches peut être modifiée pour proposer des boutons. - Changer complètement de scène ...
Aller plus loin ...
Affichage de la case
La case, quand elle est marquée , n'est pas très jolie. Si vous trouvez une belle image, vous pouvez l'intégrer comme suit.
Image bombe = new Image("file:gnome-mines.desktop.png");
gc.fillImage(bombe, x, y, w, h);
Ce qui est important dans le cas présent, c'est de savoir où placer le fichier de ressource pour qu'il soit lu par le programme Java. Si vous développez avec un éditeur simple, le répertoire par défaut où cherche la machine virtuelle est le répertoire à partir duquel la machine virtuelle est lancée. Maintenant, si vous êtes sur Eclipse, vous pouvez placer l'image dans le répertoire principal du projet. À l'exécution tout se passe comme si l'image était au bon endroit. Il faut rafraîchir (touche F5) le projet pour voir le fichier dans l'explorateur d'Eclipse
Vous auriez pu placer l'image dans le répertoire src, mais pas dans le répertoire bin qui est "reconstruit" régulièrement à partir des sources (il est d'abord effacé)
J'ai aussi affiné la couleur de la case en fonction du nombre de mines adjacentes :
- vert si la case n'est proche d'aucune bombe
- orange si la case est adjacente à une ou deux bombes
- rouge si la situation est critique.
Différer la création de la grille
La génération de la grille peut être frustrante parfois : une découverte de bombe dès le premier clic. Vous pouvez différer la dispersion des bombes au premier clic si vous en avez envie.
Modèle objet explosif
Cela peut vous frustrer que l'on exploite pas complètement le modèle objet pour les cases / cellules. On pourrait tout à fait créer
une classe Bombe
qui spécialise Case
/Cellule
. Si vous voulez savoir de quel type est la case/cellule,
on peut utiliser un opérateur comme instanceof
if (case instanceof Bombe)
System.out.println("je suis une bombe");