Plateau de jeu graphique
ChaCuN – étape 9
1. Introduction
Le but de cette étape est de continuer l'interface graphique de ChaCuN en écrivant la classe qui permet d'afficher le plateau de jeu.
2. Concepts
2.1. Plateau de jeu
Le plateau de jeu est situé dans la partie gauche de l'interface graphique de ChaCuN, dont il constitue la plus grosse partie, comme on peut le voir sur l'image ci-dessous.
Le plateau — qui porte le numéro 1 ci-dessus — est représenté par une grille de 25×25 cases carrées. Chacune de ces cases peut être soit vide, soit contenir une tuile et ses éventuels occupants.
Les cases qui contiennent une tuile affichent toujours son image. Lorsque certaines tuiles sont mises en évidence, l'image de celles qui ne le sont pas est assombrie.
2.1.1. Frange
Les cases vides dont au moins une des 4 cases voisines contient une tuile sont celles sur lesquelles la prochaine tuile pourrait être placée. Dans l'étape 5, nous avons nommé les positions de ces cases les positions d'insertion. Nous nommerons l'ensemble des cases dont la position est une position d'insertion la frange (fringe) du plateau.
Il faut noter que la frange n'existe que lorsqu'une tuile doit être placée. Lorsque ce n'est pas le cas, p. ex. car la tuile qui vient d'être placée doit être occupée, la frange est vide.
Les cases de la frange sont colorées avec la couleur du joueur actuel. De plus, si l'une de ces cases est survolée par le curseur de la souris, alors l'image de la tuile à placer apparaît sur elle. Si la tuile à placer, tournée comme elle l'est actuellement, ne peut pas être placée à cet endroit, alors son image est recouverte d'un voile blanc.
Lorsque le curseur de la souris survole une case de la frange, un clic sur le bouton droit de la souris permet de tourner la tuile à placer d'un quart de tour dans le sens antihoraire — ou dans le sens horaire si la touche Alt (Option sur Mac) est pressée simultanément. Un clic sur le bouton gauche de la souris permet de placer effectivement la tuile.
2.1.2. Occupants
Lorsqu'une tuile vient d'être placée, et que son placeur a la possibilité de l'occuper, tous les occupants potentiels de la tuile apparaissent sur elle. Le joueur courant a alors la possibilité de cliquer sur l'un de ces occupants pour le placer.
De manière similaire, lorsqu'un joueur a placé la tuile avec le chaman, il a la possibilité de cliquer sur l'un de ses pions pour le reprendre en main.
2.1.3. Jetons d'annulation
Lorsque des animaux sont annulés, par exemple suite à la pose de la fosse à pieux, leur image est recouverte d'un « jeton d'annulation », dont l'image a été présentée à l'étape précédente.
Les images des jetons d'annulation sont toujours placées entre l'image de la tuile et les occupants, afin de ne pas masquer ces derniers.
3. Mise en œuvre Java
Avant de commencer à programmer cette étape, il vous faut télécharger une archive Zip qui contient la feuille de style du plateau (board.css
). Comme les autres feuilles de style, elle doit être placée dans le dossier resources
de votre projet.
En ouvrant ce fichier dans IntelliJ, vous constaterez qu'il contient, entre autres, un grand nombre de ligne ayant pour but de placer les occupants et les jetons d'annulation sur leur tuile. Par exemple, les deux lignes suivantes, extraites de ce fichier, ont pour but de placer, sur la tuile 4, le pion occupant la zone 41 ainsi que le jeton d'annulation de l'animal 420 :
#pawn_41 { -fx-translate-x: 48; -fx-translate-y: 14 } #marker_420 { -fx-translate-x: 52; -fx-translate-y: 3 }
Vous n'avez pas besoin de comprendre la signification exacte de ces lignes, mais il vous faut juste savoir que, grâce à elles, vous n'aurez pas à placer individuellement les occupants et jetons d'annulation.
3.1. Classe BoardUI
La classe BoardUI
du sous-paquetage gui
, publique et non instanciable, contient le code de création de la partie de l'interface graphique qui affiche le plateau de jeu.
Son unique méthode publique, nommée p. ex. create
, prend en arguments la portée du plateau à créer, de type int
. En pratique, elle vaudra toujours 12 dans ce projet, mais BoardUI
doit néanmoins pouvoir être utilisable avec n'importe quelle portée strictement positive.
En plus de la portée, create
prend un certain nombre de valeurs observables en arguments. Pour alléger la présentation, leur type est donné ci-dessous sans le ObservableValue<…>
qui l'entoure. Il s'agit de :
- l'état du jeu, de type
GameState
, - la rotation à appliquer à la tuile à placer, de type
Rotation
, - l'ensemble des occupants visibles, de type
Set<Occupant>
, - l'ensemble des identifiants des tuiles mises en évidence, de type
Set<Integer>
.
Lorsque l'ensemble des identifiants des tuiles mises en évidence est vide, cela signifie qu'aucune tuile ne doit être mise en évidence, et donc que toutes les tuiles posées ont leur apparence normale.
Finalement, create
prend encore trois arguments qui sont des gestionnaires d'événements à appeler dans certaines situations. Comme précédemment, nous représenterons ces gestionnaires d'événements au moyen de l'interface Consumer
. Pour alléger la présentation, leur type est donné ci-dessous sans le Consumer<…>
qui les entoure. Il s'agit de :
- un gestionnaire prenant une valeur de type
Rotation
, à appeler lorsque le joueur courant désire effectuer une rotation de la tuile à placer, c.-à-d. qu'il effectue un clic droit sur une case de la frange, - un gestionnaire prenant une valeur de type
Pos
, à appeler lorsque le joueur courant désire poser la tuile à placer, c.-à-d. qu'il effectue un clic gauche sur une case de la frange, - un gestionnaire prenant une valeur de type
Occupant
, à appeler lorsque le joueur courant sélectionne un occupant, c.-à-d. qu'il clique sur l'un d'entre eux.
Le premier de ces gestionnaires doit être appelé soit avec Rotation.RIGHT
si la touche Alt est appuyée au moment du clic, soit avec Rotation.LEFT
sinon.
Ces gestionnaires ne doivent être appelés que si la souris ne bouge pas (trop) entre le moment où le bouton est pressé et celui où il est relâché, afin de permettre le déplacement du plateau à la souris. Cette vérification peut se faire au moyen de la méthode isStillSincePress
.
Lisez bien les conseils de programmation plus bas avant de commencer à écrire la classe BoardUI
, car ils contiennent de nombreuses indications quant à la manière d'utiliser les arguments passés à create
.
3.1.1. Graphe de scène
Le graphe de scène construit par la méthode create
— et par les auditeurs qu'elle installe — est visible dans la figure ci-dessous.
Comme les occupants et les marqueurs d'une case dépendent de la tuile qui s'y trouve, seules les cases contenant une tuile possèdent des occupants et des jetons d'annulation.
3.1.2. Conseils de programmation
- Construction du plateau
Comme nous l'avons vu plus haut, le plateau de jeu est représenté par une instance de
GridPane
qui possède un enfant par case du plateau, donc 625 enfants si la portée de ce dernier est de 12.La construction du graphe de scène se fait donc assez facilement au moyen de deux boucles imbriquées, l'une parcourant les index
x
des cases, l'autre les indexy
. À chaque itération de cette boucle, le petit graphe de scène correspondant à la case à la position (x
,y
) est construit, et ajouté à l'instance deGridPane
au moyen de sa méthodeadd
.Initialement, le graphe de scène d'une case consiste en une instance de
Group
contenant une unique instance deImageView
, qui montre une image « vide ». Un auditeur attaché à une valeur observable dont le contenu est la tuile du plateau à la position (x
,y
) augmente ce graphe de scène initial avec les nœuds correspondants aux jetons d'annulation et aux occupants au moment où une tuile est ajoutée au plateau à cette position.Afin que la position (0,0) soit initialement au centre de l'écran, il faut appeler les méthodes
setHvalue
etsetVvalue
de l'instance deScrollPane
contenant le plateau avec la valeur 0.5. - Image de fond
L'image de fond d'une case — celle qui est affichée par l'instance de
ImageView
— est soit « vide », soit l'image d'une tuile.Il faut toutefois faire attention à ce que l'on entend par « image vide ». On pourrait penser que cela signifie que la propriété
imageProperty
de l'instance deImageView
contient simplementnull
. Malheureusement, lorsque c'est le cas, l'instance deImageView
est considérée comme vide par JavaFX, de même que le groupe qui la contient. Cela implique, entre autres, qu'il est impossible de savoir quand le curseur de la souris survole la case, ce qui est problématique pour nous.Dès lors, quand une case du plateau est vide, l'instance de
ImageView
qui lui correspond affiche une image de couleur uniforme, presque blanche. Cette image, qui peut être créée une seule fois et ensuite utilisée dans toutes les cases vides, s'obtient ainsi :WritableImage emptyTileImage = new WritableImage(1, 1); emptyTileImage .getPixelWriter() .setColor(0, 0, Color.gray(0.98));
Notez que cette image a une taille de 1×1 unités, mais cela n'est pas problématique car les instances de
ImageView
que nous utilisons sont configurées pour redimensionner leur image à une taille de 128×128 unités. Cette minuscule image occupera donc la totalité de la case, comme attendu.Lorsqu'une case contient une tuile, son image de fond est simplement celle de la tuile, qui s'obtient facilement de la classe
ImageLoader
écrite à l'étape 8. Pour des raisons d'efficacité, il est toutefois nécessaire de stocker toutes les images des tuiles obtenues deImageLoader
dans une table les associant à l'identifiant de leur tuile. Cette table, que l'on nomme un cache en informatique, permet de garantir que l'image d'une tuile donnée sera chargée au plus une fois durant l'exécution du programme. Sachant que le chargement est une opération relativement lente, le cache permet d'améliorer les performances du programme. - Occupants et jetons d'annulation
Une fois ajoutés au graphe de scène, les nœuds JavaFX correspondant aux jetons d'annulation et aux occupants y restent ensuite définitivement, dans un soucis de simplicité. Toutefois, ils sont rendus visibles ou non en fonction de l'état du jeu.
Un jeton donné est visible lorsque l'identifiant de l'animal auquel il correspond fait partie des animaux annulés de l'état du jeu courant. Un occupant donné est quant à lui visible lorsqu'il fait partie de l'ensemble des occupants visibles passés à
create
. Cet ensemble contiendra tous les occupants effectivement posés sur le plateau ainsi que tous les occupants potentiels de la dernière tuile lorsque celle-ci doit être occupée.Comme illustré à la figure 2, l'identité d'un nœud JavaFX représentant un jeton d'annulation est la concaténation de la chaîne
marker_
avec l'identifiant de l'animal auquel le jeton correspond. De même, l'identité d'un nœud JavaFX représentant un occupant est la concaténation de la chaînepawn_
(pour un pion), ou de la chaînehut_
(pour une hutte), avec l'identifiant de la zone qu'il occupe.Les nœuds JavaFX représentant les occupants — mais pas ceux représentant les jetons — subissent une rotation de manière à apparaître toujours verticaux à l'écran, même si la tuile qu'ils occupent est tournée.
Finalement, un clic sur un nœud JavaFX représentant un occupant provoque toujours l'appel de la méthode
accept
du troisième gestionnaire d'événements passé àcreate
, avec l'occupant lui-même en argument. - Rotation
Les tuiles posées sur le plateau peuvent être tournées d'un nombre quelconque de quarts de tour. Bien entendu, cela implique que leur image doit également l'être.
Toutefois, tourner uniquement l'image de la tuile, par exemple en appliquant une rotation à l'instance de
ImageView
qui la contient, ne suffit pas. En effet, les éventuels jetons d'annulation et les occupants doivent également être tournés, faute de quoi ils ne sont pas positionnés correctement.Dès lors, lorsqu'une tuile a été tournée avant d'être posée sur le plateau, la rotation doit être appliquée à l'instance de
Group
qui représente la case qui la contient. C'est pour cette raison que les occupants doivent subir la rotation inverse de celle appliquée au groupe les contenant.Appliquer une rotation à un nœud est très simple avec JavaFX, puisqu'il suffit de modifier le contenu de sa propriété
rotateProperty
. - Voile
Comme nous l'avons dit dans l'introduction, les cases sont parfois recouvertes d'un voile coloré, c.-à-d. d'une image semi-transparente de couleur uniforme. Il y a trois situations dans lesquelles c'est le cas :
- si la case contient une tuile, et que certaines tuiles sont mises en évidence mais pas celle de la case, alors elle est recouverte d'un voile noir qui l'assombrit,
- si la case fait partie de la frange mais quelle n'est pas survolée par le curseur de la souris, alors elle est recouverte d'un voile de la couleur du joueur courant,
- si la case fait partie de la frange, que le curseur de la souris la survole et que la tuile courante, avec sa rotation actuelle, ne peut pas y être placée, alors elle est recouverte d'un voile blanc.
Avec JavaFX, le voile peut être représenté par un effet graphique attaché à l'instance de
Group
qui représente la case, au moyen de sa propriétéeffectProperty
. L'effet qui convient ici est un mélange (Blend
) de typeSRC_OVER
entre l'image du voile, à l'avant plan, et l'image de la tuile, à l'arrière-plan. L'image du voile peut être créée au moyen d'une instance deColorInput
de la taille d'une tuile, dont l'opacité est de 0.5 et la couleur est celle du voile. - Représentation d'une case
Comme expliqué dans les sections qui précèdent, la représentation graphique d'une case est paramétrée par trois éléments :
- l'image de fond de la case,
- l'éventuelle rotation appliquée à la case,
- la couleur de l'éventuel voile recouvrant la case.
Ces trois éléments dépendent d'un assez grand nombre de valeurs qui changent au cours du temps, comme le contenu du plateau, la frange, la rotation appliquée à la tuile à poser, le fait que le curseur de la souris survole la case, etc.
Pour représenter ces trois éléments, nous vous conseillons vivement de définir un enregistrement privé, nommé p. ex.
CellData
, les regroupant. Cela fait, vous pouvez définir, pour chaque case du plateau, une valeur observable contenant l'instance de cet enregistrement correspondant à la case. Comme cette valeur observable dépend de plusieurs autres valeurs observables, et pas d'une seule, il n'est pour une fois pas possible d'utilisermap
pour la définir. Au lieu de cela, il vous faut utiliser la méthodecreateObjectBinding
de la classeBindings
, que l'on peut voir comme une variante demap
généralisée à plusieurs arguments.Le premier argument passé à
createObjectBinding
est une lambda sans argument dont le but est de calculer la valeur observable à créer, généralement en fonction de plusieurs autres valeurs observables. Les autres arguments decreateObjectBinding
sont toutes les valeurs observables utilisées dans la lambda.Par exemple, admettons que l'on dispose de deux valeurs observables,
string1O
etstring2O
, contenant des chaînes de caractères, et que l'on désire créer une autre valeur observable contenant la concaténation de ces chaînes. Cela peut se faire ainsi :ObservableValue<String> string1O = …; ObservableValue<String> string2O = …; ObservableValue<String> string12O = Bindings .createObjectBinding(() -> { return string1O.getValue() + string2O.getValue(); }, string1O, string2O);
Comme cet exemple l'illustre,
createObjectBinding
permet de faire quelque chose de similaire à ce que l'on fait dans un tableur lorsqu'on place une formule dans une cellule, la formule en question étant le corps de la lambda. Une différence importante est qu'un tableur peut lui-même déterminer les dépendances de la formule (icistring1O
etstring2O
), alors qu'elles doivent être passées explicitement àcreateObjectBinding
.Notez que, comme dit plus haut, l'une des choses dont dépend la représentation d'une case est le fait que le curseur de la souris la survole ou non. Cette information est disponible sous la forme d'une propriété nommée
hoverProperty
que tout nœud JavaFX possède.
3.2. Tests
Le programme de test ci-dessous, dont vous pouvez vous inspirer, est similaire à celui de l'étape 8 mais construit l'interface d'un plateau de jeu d'une portée de 1.
public final class BoardUITest extends Application { public static void main(String[] args) {launch(args);} @Override public void start(Stage primaryStage) throws Exception { var playerNames = /* comme à l'étape 8 */; var playerColors = /* comme à l'étape 8 */; var tileDecks = /* comme à l'étape 8 */; var textMaker = /* comme à l'étape 8 */; var gameState = /* comme à l'étape 8 */; var tileToPlaceRotationP = new SimpleObjectProperty<>(Rotation.NONE); var visibleOccupantsP = new SimpleObjectProperty<>(Set.<Occupant>of()); var highlightedTilesP = new SimpleObjectProperty<>(Set.<Integer>of()); var gameStateO = new SimpleObjectProperty<>(gameState); var boardNode = BoardUI .create(1, gameStateO, tileToPlaceRotationP, visibleOccupantsP, highlightedTilesP, r -> System.out.println("Rotate: " + r), t -> System.out.println("Place: " + t), o -> System.out.println("Select: " + o)); gameStateO.set(gameStateO.get().withStartingTilePlaced()); var rootNode = new BorderPane(boardNode); primaryStage.setScene(new Scene(rootNode)); primaryStage.setTitle("ChaCuN test"); primaryStage.show(); } }
En exécutant ce programme, vous devriez voir une fenêtre similaire à celle ci-dessous s'afficher à l'écran. Certains aspects pourraient bien entendu différer si vous n'avez pas encore terminé la programmation de votre classe.
La vidéo ci-dessous montre le comportement de ce programme lorsque la souris survole les cases de la frange, et lorsqu'on clique sur elles au moyen de l'un des deux boutons, éventuellement en maintenant la touche Alt pressée en même temps. Les cercles rouges qui apparaissent lors des clics ont été ajoutés lors de la création de la vidéo, et ne doivent bien entendu pas être visibles dans votre version.
N'hésitez pas à modifier ce programme de test pour, par exemple, ajouter des tuiles au plateau, vérifier que l'affichage des occupants fonctionne, que les animaux peuvent être annulés, etc.
4. Résumé
Pour cette étape, vous devez :
- écrire la classe
BoardUI
selon les indications données ci-dessus, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies.
Aucun rendu n'est à faire pour cette étape avant le rendu final. N'oubliez pas de faire régulièrement des copies de sauvegarde de votre travail en suivant nos indications à ce sujet.