Gestion et animation des tuiles
Ajul – étape 10
1. Introduction
Le but de cette étape est d'écrire les classes gérant l'interaction avec les tuiles et l'animation de leur déplacement au-dessus du plateau.
2. Concepts
2.1. Affichage des tuiles
Comme nous l'avons vu à l'étape 8, les tuiles sont représentées à l'écran par de simples rectangles colorés. Ces rectangles sont placés au-dessus du plateau de jeu, qu'ils masquent donc partiellement.
Chaque tuile occupe un emplacement, et chaque emplacement contient au maximum une tuile. Les emplacements sont ceux décrits à l'étape 8 et, pour mémoire, il en existe de cinq types :
- hors du plateau, un tel emplacement étant caractérisé par une sorte de tuile et un index,
- sur une source, un tel emplacement étant caractérisé par l'identité de la source et l'index de l'emplacement dans la source,
- sur une ligne de motif, un tel emplacement étant caractérisé par l'identité du joueur auquel la ligne appartient, l'identité de la ligne elle-même, et l'index de l'emplacement dans la ligne,
- sur un mur, un tel emplacement étant caractérisé par l'identité du joueur auquel le mur appartient, l'identité de la ligne de motif dans le prolongement de laquelle se trouve l'emplacement, et la couleur de la tuile qu'il peut accueillir,
- sur une ligne de plancher, un tel emplacement étant caractérisé par l'identité du joueur auquel la ligne appartient, et l'index de l'emplacement dans la ligne.
Dans l'interface graphique, à tous les emplacements des quatre derniers types correspond une ancre, qui est un rectangle de la même taille qu'une tuile, parfois invisible, intégré au plateau de jeu. Ces ancres facilitent le placement des tuiles à l'écran, car pour positionner une tuile qui occupe un emplacement donné, il suffit de la placer exactement au-dessus de l'ancre correspondante.
2.2. Affichage des points
Les joueurs d'une partie d'Ajul obtiennent des points de deux manières différentes : à la fin d'une manche, pour chaque tuile ajoutée à leur mur ; et à la fin de la partie, pour chaque ligne, colonne ou couleur complète dans leur mur.
Comme nous l'avons vu à l'étape précédente, les points de fin de partie sont affichés directement sur le plateau de jeu, en bordure du mur. Par contre, les points de fin de manche sont affichés au-dessus des tuiles du mur. Cela est visible dans l'image ci-dessous, qui montre un mur contenant 17 tuiles, les points rapportés par chacune d'elles lui étant superposés.
2.3. Interaction avec les tuiles
Les tuiles sont le principal moyen pour les joueurs humains d'interagir avec Ajul puisque le jeu se fait par glisser/déposer de tuiles. C'est-à-dire que, lorsque c'est son tour, un joueur humain procède ainsi pour jouer son coup :
- il clique sur une tuile colorée se trouvant dans l'une des sources, et garde le bouton pressé,
- il déplace la tuile vers une destination de son plateau à même de l'accueillir — les autres tuiles de la même source et de la même couleur suivant automatiquement celle déplacée explicitement,
- il relâche le bouton sur la destination désirée.
Lorsque des tuiles sont ainsi déplacées, les destinations à même de les accueillir se mettent en évidence lorsqu'elles sont survolées.
2.4. Animation des tuiles
Les tuiles qui ne sont pas directement déplacées par un joueur humain de la manière décrite ci-dessus sont déplacées automatiquement au fur et à mesure de l'avancement de la partie. Il s'agit par exemple des tuiles se trouvant sur la même fabrique que celles jouées, mais d'une autre couleur, et qui doivent être déplacées vers la zone centrale ; des tuiles déplacées dans le mur à la fin d'une manche ; des tuiles jouées par l'intelligence artificielle ; etc. Pour faciliter la compréhension du déroulement de la partie, le déplacement de ces tuiles est animé.
Ces animations, fort utiles pour les humains qui suivent la partie, ne sont toutefois pas évidentes à mettre en œuvre. En effet, dans la représentation de l'état de la partie que nous avons utilisée, les tuiles n'ont pas d'identité et il n'est donc pas directement possible de savoir comment elles se déplacent.
Par exemple, si un joueur joue un coup dont l'effet est de déplacer 2 tuiles de couleur A sur sa seconde ligne de motif, et 2 tuiles de couleur B sur la zone centrale, seuls les compteurs associés à ces sources et destinations changent dans l'état de la partie. Il n'y a aucun moyen de savoir, par exemple, laquelle des 2 tuiles de couleur A a été placée dans la première case de la ligne de motif, car cette information n'existe tout simplement pas dans l'état de la partie tel que nous le représentons.
Il faut dès lors trouver un moyen de reconstruire cette information pour animer le déplacement des tuiles conformément aux règles du jeu et de manière aussi plaisante que possible. Par exemple, dans la situation décrite ci-dessus, il serait bien que la tuile de couleur A qui se trouve le plus à gauche dans la fabrique se retrouve dans l'emplacement le plus à gauche de la ligne de motif. Sinon, elle et sa voisine se croiseraient sans raison durant l'animation, ce qui ne serait pas plaisant.
Une première constatation que l'on peut faire au sujet de ces déplacements est que les six sortes de tuiles peuvent être traitées de manière totalement indépendantes. Dès lors, dans ce qui suit, nous parlerons toujours de tuiles sans spécifier leur sorte, en sous-entendant que chacune des six sortes de tuiles doit être traitée successivement de la même manière.
L'idée de base de la technique que nous utiliserons est de déterminer, chaque fois que l'état de la partie changera, deux ensembles d'emplacements :
- celui des emplacements actuellement occupés par une tuile à l'écran,
- celui des emplacements qui devraient être occupés par une tuile, selon l'état actuel de la partie.
Si ces deux ensembles sont égaux, alors les tuiles sont correctement placées à l'écran, et il n'y a rien à faire. Par contre, s'ils diffèrent, cela signifie que certaines tuiles ne sont pas à leur place, et on peut donc calculer deux autres ensembles, qui sont :
- l'offre (supply), qui est l'ensemble des emplacements occupés par une tuile mais qui ne devraient pas l'être,
- la demande (demand), qui est l'ensemble des emplacements qui ne sont pas occupés par une tuile, mais qui devraient l'être.
Ces deux ensembles ont forcément la même taille, car à toute tuile mal placée correspond un élément de l'offre — l'emplacement sur lequel cette tuile se trouve — et un élément de la demande — l'emplacement sur lequel cette tuile devrait se trouver.
Une fois ces ensembles calculés, il ne reste plus qu'à apparier chaque élément de l'offre avec un élément de la demande. Cela fait, les tuiles peuvent être animées en déplaçant chacune des tuiles qui occupe un emplacement de l'offre — et qui est donc mal placée — vers l'emplacement de la demande qui lui est apparié.
Si nous désirions simplement déplacer les tuiles sans nous soucier du fait que ces déplacements soient conformes aux règles du jeu ou plaisants, nous pourrions faire cet appariement de manière quelconque. Pour obtenir des déplacements conformes aux règles et plaisants, il est toutefois nécessaire d'utiliser un algorithme d'appariement plus sophistiqué, décrit ci-dessous.
2.5. Algorithme d'appariement
Pour apparier les éléments de l'offre et de la demande de manière idéale, nous ferons l'hypothèse que les tuiles qui ne sont pas correctement placées le sont en raison d'un passage d'un état de partie à un état suivant. C'est-à-dire que les appariements qu'il produit sont ceux qui correspondent au jeu d'un coup par un joueur, au vidage des lignes de motif et plancher en fin de manche, ou au remplissage des fabriques en début de manche.
L'idée de cet algorithme est de satisfaire la demande dans un ordre précis afin que les appariements produisent des mouvements qui correspondent aux règles du jeu. L'algorithme procède ainsi :
- chaque élément de la demande du mur — c.-à-d. les emplacements de la demande qui se trouvent sur un mur — est apparié avec l'élément correspondant de l'offre, à savoir celui qui se trouve sur le plateau du même joueur, sur la ligne de motif correspondant à l'emplacement, dans la case la plus à droite,
- chaque élément de la demande de plancher est apparié avec le prochain élément de l'offre d'une source,
- chaque élément de la demande de ligne de motif est apparié avec le prochain élément de l'offre d'une source,
- tous les éléments de la demande hors du plateau qui le peuvent sont appariés avec les éléments de l'offre du plancher qui restent,
- tous les éléments de la demande hors du plateau qui le peuvent sont appariés avec les éléments de l'offre d'une source qui restent,
- le reste de l'offre est apparié de manière quelconque avec le reste de la demande.
Lors de ces appariements, les différentes parties de la demande — c.-à-d. la demande du mur, celle des lignes de motif, etc. — sont triées par index croissant. De plus, les éléments de l'offre de source sont triés par ordre décroissant de source (premier critère) puis par ordre croissant d'index (second critère). Chaque fois qu'un élément de l'offre et de la demande doivent être appariés, ce sont les deux premiers de leur ensembles respectifs qui le sont.
3. Mise en œuvre Java
Cette étape n'étant à nouveau pas évidente à programmer, nous vous conseillons de la mettre en œuvre progressivement. Commencez par exemple par écrire une version de TileAnimator effectuant les appariements de manière quelconque, puis écrivez une version basique de TileOverlayUI qui ne gère pas le déplacement des tuiles mais seulement leur affichage. Ensuite, écrivez un programme de test similaire à celui proposé à la §3.3, vérifiez que le code que vous avez écrit semble correct, puis ajoutez progressivement les éléments manquants.
3.1. Classe TileAnimator
La classe TileAnimator du sous-paquetage gui, publique et finale, a pour but d'animer le déplacement des tuiles d'une manière conforme aux règles du jeu, et plaisante. Elle ne possède qu'une seule méthode publique (et statique) nommée p. ex. animateTiles, qui prend en arguments :
- une fonction permettant d'obtenir la position à laquelle placer une tuile étant donné son emplacement, de type
Function<TileLocation, Point2D>, - une table associant à chaque sorte de tuile la liste des nœuds représentant ces tuiles, de type
Map<TileKind, List<Node>>, - un état de partie, de type
ReadOnlyGameState,
et retourne une animation, de type Animation, qui déplace les tuiles afin qu'elles occupent les positions qu'elles doivent occuper étant donné l'état de la partie.
3.1.1. Conseils de programmation
- Partition des emplacements
L'algorithme d'appariement décrit à la §2.5 a souvent besoin d'accéder à un type particulier d'offre ou de demande, p. ex. la demande du mur. Il est dès lors conseillé de partitionner les emplacements de l'offre et de la demande en cinq listes contenant chacune les emplacements d'un type donné. Par exemple, l'une de ces listes pourrait contenir ce que nous avons appelé la demande du mur, et avoir donc le type
List<TileLocation.OnWall>. Ces cinq listes peuvent être regroupées dans un enregistrement privé de la classeTileAnimator. - Calcul de l'offre et de la demande
Une fois que la totalité des emplacements sur lesquels une tuile devrait se trouver ont été calculés, il faut en déterminer l'offre et la demande en regardant lesquels de ces emplacements contiennent déjà une tuile.
Une manière élégante de faire cela est de considérer initialement que la demande est constituée de tous les emplacements qui devraient contenir une tuile dans l'état actuel de la partie. Ensuite, les tuiles présentes sur le plateau sont parcourues, et pour chacune d'elles :
- si l'emplacement qu'elle occupe fait partie de la demande, alors il en est retiré,
- sinon, l'emplacement est ajouté à l'offre.
Malheureusement, le fait que les emplacements de l'offre et de la demande soient partitionnés en cinq listes pose un petit problème lors de la mise en œuvre de cette idée. En effet, lorsqu'on obtient l'emplacement sur lequel se trouve une tuile, on ne sait pas dans laquelle des cinq listes il pourrait se trouver.
Pour savoir cela, il faut examiner le type exact de l'emplacement, et s'il s'agit par exemple d'un emplacement de type
OnWall, en conclure que la liste dans laquelle il doit être recherché est celle contenant tous les emplacements de ce type-là. Déterminer le type exact de l'emplacement peut bien entendu se faire au moyen d'une séquence de testsinstanceof, ainsi :TileLocation loc = …; // emplacement occupé par la tuile if (loc instanceof TileLocation.OnWall onWall) // … rechercher `onWall` dans la liste correspondante else if (loc instanceof TileLocation.OnPattern onPattern) // … rechercher `onPattern` dans la liste correspondante else if (/* … et ainsi de suite */)
Toutefois, Java offre depuis peu ce que l'on nomme le filtrage de motifs (pattern matching) dans l'énoncé
switch, qui permet d'écrire le code ci-dessus de manière plus concise et élégante, ainsi :TileLocation loc = …; // emplacement occupé par la tuile switch (loc) { case TileLocation.OnWall onWall -> // … rechercher `onWall` dans la liste correspondante case TileLocation.OnPattern onPattern -> // … rechercher `onPattern` dans la liste correspondante case /* … et ainsi de suite */ -> }Le filtrage de motifs ne sera pas examiné au cours, mais les personnes intéressées à l'utiliser dans cette étape pourront se rapporter au guide Pattern matching with switch.
- Animation des tuiles
Une fois l'offre et la demande déterminées, on peut leur appliquer l'algorithme décrit à la §2.5 afin d'apparier leurs éléments. Les appariements peuvent être stockés dans une table associative dont les clefs sont les éléments de l'offre, et les valeurs sont les éléments de la demande correspondants.
À partir de cette table, il n'est plus très difficile de construire l'animation des tuiles. Celle-ci consiste en une instance de
ParallelTransitiondont les enfants sont les animations individuelles correspondant aux différentes tuiles, que l'on désire déplacer simultanément. Chacune de ces animations de tuile est une instance deRelocationTransition, l'animation écrite à l'étape 8 et permettant d'animer le déplacement d'un nœud JavaFX de sa position actuelle à une position d'arrivée choisie.
3.2. Classe TileOverlayUI
La classe TileOverlayUI du sous-paquetage gui, publique et finale, gère la représentation graphique des tuiles à l'écran.
Son constructeur est privé, la création d'une instance devant se faire au moyen d'une méthode fabrique nommée p. ex. create et prenant en arguments :
- une valeur observable contenant un état de partie immuable, de type
ObservableValue<ImmutableGameState>, - l'objet contenant les ancres et les tuiles, de type
Tiles, - un ensemble contenant les coups valides qu'un joueur humain pourrait jouer, destiné uniquement à être lu (et pas modifié), de type
Set<Move>, - un ensemble destiné à contenir le sous-ensemble des coups valides qui peuvent effectivement être joués, et qui est destiné à être rempli chaque fois que le joueur commence à déplacer une tuile,
- une valeur booléenne stockée dans un tableau de type
boolean[], destinée à être testée au moment où l'utilisateur relâche le bouton de la souris lors d'un déplacement de tuiles, et qui sera alors vraie ssi ce relâchement aura été fait au-dessus d'une destination à même de les recevoir.
Les deux dernières valeurs sont les mêmes que celles passées à la méthode create de la classe BoardUI écrite à l'étape précédente, et leur but est de permettre la communication entre ces deux parties du programme.
Au moyen des valeurs ci-dessus, la méthode create se charge de :
- créer le panneau destiné à contenir les tuiles et les points qu'elles ont rapportés, décrit à la §3.2.1 plus bas,
- ajouter tous les nœuds représentant les tuiles à ce panneau, en les plaçant initialement hors du plateau (
TileLocation.OffBoard) et en les positionnant aux coordonnées (-30, -30) attribuées à ces emplacements, - attacher des gestionnaires d'événements à ces nœuds de manière à permettre le déplacement des tuiles depuis les sources vers les destinations, comme décrit à la §3.2.2.
En plus de cette méthode fabrique, TileOverlayUI offre deux méthodes publiques, qui sont :
- une méthode, nommée p. ex.
root, retournant le nœud JavaFX à la racine du graphe de scène décrit à la §3.2.1, - une méthode, nommée p. ex.
showTilePoints, prenant en arguments un emplacement sur le mur d'un joueur, de typeTileLocation.OnWall, et un nombre de points, de typeint, et affichant ce nombre de points au-dessus de l'emplacement, comme sur la figure 1.
3.2.1. Graphe de scène
Le graphe de scène de la partie de l'interface gérée par TileOverlayUI est extrêmement simple et consiste en un unique panneau de type Pane dont les enfants sont les nœuds représentant les tuiles et ceux indiquant les points rapportés par les tuiles du mur.
Pour savoir comment positionner à l'écran les nœuds représentant les tuiles et les points, voir les conseils de programmation plus bas.
3.2.2. Gestion des événements
Initialement, deux gestionnaires d'événements doivent être attachés à chaque nœud représentant une tuile colorée :
- un gestionnaire pour l'événement
MOUSE_PRESSED, qui ne fait rien d'autre qu'appeler la méthodesetDragDetectavec la valeur vraie, afin que les tuiles puissent être déplacées immédiatement, - un gestionnaire pour l'événement
DRAG_DETECTED, qui gère le déplacement des tuiles.
Ces gestionnaires peuvent être installés au moyen des méthodes set… qui leur correspondent, à savoir setOnMousePressed et setOnDragDetected. Le second d'entre eux, qui gère le déplacement des tuiles, est relativement complexe car il doit :
- vérifier que la tuile sur laquelle l'événement s'est produit se trouve bien sur une source, et que l'ensemble des coups valides n'est pas vide, ce qui signifie que c'est bien à un joueur humain de jouer — et arrêter le traitement de l'événement si ce n'est pas le cas,
- calculer le sous-ensemble des coups valides qui correspondent à la couleur de la tuile sur laquelle l'événement s'est produit, et à la source sur laquelle elle se trouve ; pour mémoire, cet ensemble permet aux destinations de déterminer si elles sont à même d'accueillir les tuiles déplacées par le joueur humain,
- faire en sorte que les destinations soient informées lorsqu'elles sont survolées par les tuiles déplacées, en appelant la méthode
startFullDragdu nœud sur lequel l'événement s'est produit, - rendre le panneau contenant les tuiles « transparent à la souris » au moyen de sa méthode
setMouseTransparent, afin qu'il n'empêche pas les destinations, qui se trouvent sous lui, de recevoir les événements souris, - déterminer l'ensemble des nœuds à déplacer, qui est constitué de tous ceux représentant des tuiles de même couleur et se trouvant sur la même source que celui sur lequel l'événement s'est produit,
- faire en sorte que ces nœuds déplacés apparaissent au-dessus des autres,
- installer, pour la durée du déplacement, deux nouveaux gestionnaires d'événements sur le nœud sur lequel l'événement s'est produit, décrits ci-après.
Les deux gestionnaires supplémentaires à installer pour la durée du déplacement concernent les événements MOUSE_DRAGGED et MOUSE_DRAG_DONE.
L'événement MOUSE_DRAGGED se produit chaque fois que le pointeur de la souris est déplacé alors que le bouton est encore pressé, et son gestionnaire doit donc déplacer les nœuds représentant les tuiles afin qu'ils suivent le pointeur. Cela peut se faire aisément en changeant la translation associée à ces nœuds — au moyen des méthodes setTranslateX et setTranslateY — de manière à ce qu'elle soit toujours égale au déplacement de la souris depuis le clic initial, comme décrit dans les conseils de programmation plus bas. Notez que la position du pointeur de la souris au moment où un événement se produit est stockée dans l'événement, ce qui est fort utile ici.
L'événement MOUSE_DRAG_DONE se produit à la fin du déplacement, lorsque l'utilisateur relâche le bouton de la souris. Son gestionnaire doit donc :
- déterminer si les tuiles ont été relâchées sur une destination à même de les accueillir, et :
- si oui, transférer la translation actuelle des nœuds les représentant dans leur « translation de mise en page » — comme expliqué dans les conseils de programmation plus bas,
- si non, remettre les tuiles à leur place initiale, au moyen d'une animation de type
TranslateTransitiondurant un huitième de seconde,
- annuler tous les changements qui ont été faits pour la durée du déplacement, comme l'ajout temporaire de gestionnaires d'événements, le placement des tuiles déplacées devant les autres, le changement de la transparence à la souris du panneau, etc.
3.2.3. Conseils de programmation
- Positionnement des tuiles et des points
La position d'un nœud JavaFX à l'écran est exprimée dans le système de coordonnées de son parent. Dans le cas des tuiles et des points, cela signifie que la position des nœuds les représentant est exprimée dans le système de coordonnées du panneau qui les contient.
Nous l'avons dit, une tuile se trouve toujours sur un emplacement donné. Si cet emplacement est hors du plateau, alors la position correspondante est celle dont les coordonnées sont (-30, -30). Sinon, la position correspondante est celle qui place la tuile au-dessus de l'ancre associée à cet emplacement.
Placer une tuile au-dessus d'une ancre ne peut toutefois pas se faire juste en attribuant au nœud la représentant la position de l'ancre correspondante, car la position de l'ancre est exprimée dans le système de coordonnées de son parent à elle. Il faut donc effectuer un changement de système de coordonnées pour transformer la position de l'ancre dans le système de coordonnées du panneau contenant les tuiles. Avec JavaFX, cela peut se faire en :
- obtenant la position de l'ancre dans le système de coordonnées de la scène — qui contient toute l'interface graphique — au moyen de la méthode
localToScene, - transformant cette position-là, exprimée dans le système de coordonnée de la scène, dans celui du panneau, au moyen de la méthode
sceneToLocal.
Une fois la position du nœud représentant une tuile déterminée, il reste à savoir comment l'attribuer au nœud. Pour cela, il faut savoir qu'avec JavaFX, la position d'un nœud est déterminée par la somme de deux translations, qui sont :
- la « translation de mise en page », dont les composantes se nomment
layoutXetlayoutY, qui est celle modifiée par la méthoderelocate, - la translation (tout court), dont les composantes se nomment
translateXettranslateY.
Nous utiliserons la translation de mise en page pour placer les tuiles au-dessus de leur ancre, et la seconde translation pour les faire suivre le pointeur de la souris lors d'un déplacement des tuiles par l'utilisateur. L'intérêt de procéder ainsi est que, si l'utilisateur relâche les tuiles ailleurs que sur une destination à même de les accepter, il est très facile de les remettre à leur place en réinitialisant la seconde translation.
En conséquence, lorsque le gestionnaire de l'événement
MOUSE_DRAGGEDdoit déplacer un groupe de tuiles, il le fait en changeant leur translation au moyen des méthodessetTranslateXetsetTranslateY. De même, lorsque le gestionnaire de l'événementMOUSE_DRAG_DONEest informé que l'utilisateur a relâché le bouton de la souris, il détermine si cela s'est produit alors qu'une destination à même d'accepter les tuiles était survolée et :- si oui, laisse les tuiles en place mais transfère leur translation actuelle dans la translation de mise en page, afin que leur déplacement vers leur position finale soit animé depuis là,
- si non, remet les tuiles à leur position de départ au moyen d'une animation de type
TranslateTransitionqui réinitialise à 0 la translation.
Pour mémoire, la classe
TileAnimatorutilise une animation de typeRelocationTransitionpour déplacer les tuiles, et cette animation modifie la translation de mise en page pour la faire passer de sa valeur actuelle à une valeur finale donnée. - obtenant la position de l'ancre dans le système de coordonnées de la scène — qui contient toute l'interface graphique — au moyen de la méthode
- Couches d'affichage
Les tuiles en cours de déplacement doivent apparaître au-dessus des tuiles immobiles, et les points rapportés par les tuiles du mur doivent apparaître au-dessus d'elles.
Pour garantir cela, le plus simple est de considérer que chaque enfant du panneau contenant les tuiles et les points se trouve dans l'une des trois « couches de nœuds » qui existent : celle contenant les tuiles immobiles, celle contenant les tuiles en mouvement, et celle contenant les points.
À chacune de ces couches correspond un ordre de dessin (view order), qui peut par exemple valoir 0 pour celle des tuiles immobiles, -1 pour celle des tuiles mobiles et -2 pour celle des points. La méthode
setViewOrderpeut être utilisée pour changer l'ordre de dessin d'un nœud, et donc le déplacer (conceptuellement) d'une couche à une autre. - Ajout et centrage des points
La méthode
showTilePointsdoit créer une nouvelle instance deTextreprésentant les points, l'ajouter aux enfants du panneau contenant les tuiles et les points, la placer dans la bonne couche et la positionner au centre de la tuile.Pour centrer le texte dans la tuile, il faut déterminer sa taille, qui peut s'obtenir en appliquant la méthode
getBoundsInLocalà l'instance deTextle représentant.L'ajout de cette instance aux enfants du panneau pose un petit problème, car JavaFX exige que toutes les modifications au graphe de scène soient faites dans le fil d'exécution JavaFX (JavaFX thread). Nous verrons à l'étape suivante ce que cela signifie exactement, pour l'instant il suffit de savoir que cela implique d'effectuer l'ajout dans une lambda passée à la méthode
runLaterdePlatform, ainsi :// `root` est le panneau contenant les tuiles et points Platform.runLater(() -> root.getChildren().add(…));
- Observation de l'état de la partie
La valeur observable contenant l'état de la partie passée à la méthode
createdoit bien entendu être observée afin que les tuiles soient déplacées chaque fois que l'état de la partie change.Pour cela, il est conseillé d'utiliser la méthode
subscribe, très similaire à la méthodeaddObserverdu patron Observer, à une différence (importante) près : lorsqu'on l'appelle, elle ne se contente pas d'ajouter l'observateur à la liste des observateurs de la valeur observable, mais elle appelle aussi sa méthode de mise à jour avec la valeur initiale de cette valeur observable. Cela est généralement très utile, car cela facilite l'initialisation de l'application.Dans ce cas précis, il faut toutefois faire attention à un petit problème : au moment où
createest appelée, l'interface graphique d'Ajul n'est pas encore mise en page, et les ancres n'ont donc pas leur position correcte à l'écran. Il n'est donc pas encore possible de les utiliser pour positionner les tuiles, en particulier le marqueur de premier joueur.Pour résoudre ce problème, il est possible de retarder un peu l'appel de la méthode
subscribe, en utilisant une fois encore la méthoderunLater. Cela nous permet de garantir que les ancres seront correctement placées au moment oùsubscribeappellera la méthode de l'observateur avec la valeur initiale du sujet. Le code ressemble donc à ceci :Platform.runLater(() -> gameStateO.subscribe(…));
où
gameStateOest la valeur observable passée àcreate.
3.3. Tests
Pour tester cette étape, vous pouvez écrire une classe similaire à la classe TestBoardUI de l'étape précédente, comme illustré ci-dessous. Notez que cet extrait de code n'est pas complet, et que vous devez encore écrire la partie signalée par un commentaire TODO.
public final class TestStage10 extends Application {
@Override
public void start(Stage primaryStage) {
// … comme `TestBoardUI`, mais `gameStateP` a le type
// `ObjectProperty<…>`, pas `ObservableValue<…>`.
Set<Move> validMoves = new HashSet<>();
TileOverlayUI tileOverlayUI =
TileOverlayUI.create(gameStateP,
tiles,
validMoves,
potentialMoves,
moveAccepted);
Parent root = new StackPane(boardUI.root(),
tileOverlayUI.root());
Platform.runLater(() -> {
MutableGameState gameState =
new MutableGameState(initialGameState);
gameState.fillFactories(RandomGeneratorFactory
.getDefault()
.create(2026));
ImmutableGameState immutableGameState =
gameState.immutable();
// TODO remplir `validMoves` avec les coups valides.
gameStateP.set(immutableGameState);
});
root.getStylesheets().add("ajul.css");
// … comme `TestBoardUI`
}
}
4. Résumé
Pour cette étape, vous devez :
- écrire les classes
TileAnimatoretTileOverlayUIselon les indications données plus haut, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies.
Vous n'avez pas l'obligation de rendre cette étape avant le rendu final, mais nous vous conseillons néanmoins de le faire afin de sauvegarder votre travail.