Index de l'article

Préambule :

  • Tout ce tutoriel est basé sur un ordinateur avec une distribution type debian/ubuntu, un SDK Java du type openjdk+openjfx 11/17, et l'IDE Intellij IDEA.
  • Ces hypothèses sont valables notamment pour les machines du département.
  • Si vous utilisez votre propre ordinateur sous windows/mac, les procédures d'installation logicielle seront relativement semblables mais avec des différences notables, par exemple dans les chemins d'accès aux éléments installés.
  • De même, si vous installez un sdk ou une version de javaFx différente, vous vous exposez à des incompatibilités avec le framework boardifier. 

 

  • Pour suivre ce tutoriel, il faut d'abord suivre celui qui implémente le jeu en mode console : Tutoriel 1 : créer un jeu de plateau en mode texte avec boardifier-console
  • En effet, seules les différences dans la modélisation et l'implémentation des classes des parties modèle, vue et contrôleur sont abordées ici.
  • Comme on va le constater, ces différences ne sont pas très importantes, et elles concernent principalement :
    • la classe principale du jeu,
    • les classes de la partie vue permettant de représenter visuellement les jetons et le plateau de jeu,
    • la gestion des interactions avec la souris.
  • A côté de cela, des modifications mineures doivent être faites sur les classes du modèle, notamment pour tenir compte de la sélection d'un pion avec la souris.

 

L'objectif de de tutoriel est de présenter le développement d'un jeu de plateau grâce à Boardifier. C'est un framework de développement Java basé sur la bibliothèque graphique JavaFx. C'est le "grand frère" de boardifier-console, car il en reprend les principes essentiels, notamment l'utilisation du paradigme MVC strict, mais avec des classes basées sur Javafx pour la mise en place des parties vue et contrôle. Malgré cela, la partie modèle est identique, ainsi qu'environ 80% du code des classes de contrôle et de vue est identique. C'est pourquoi il est relativement aisé de passer d'une version console à une version graphique, puisque tout le côté fonctionnel peut être repris.

 

Pour suivre ce tutoriel, il faut donc d'abord suivre celui qui implémente le jeu en mode console : Tutoriel 1 : créer un jeu de plateau en mode texte avec boardifier-console.

Dans la suite de cet article, seules les différences dans la modélisation et l'implémentation des classes des parties modèle, vue et contrôleur sont abordées. Comme on va le constater, ces différences ne sont pas très importantes, et elles concernent principalement :

  • la classe principale du jeu,
  • les classes de la partie vue permettant de représenter visuellement les jetons et le plateau de jeu,
  • la gestion des interactions avec la souris.

A côté de cela, des modifications mineures doivent être faites sur les classes du modèle, notamment pour tenir compte de la sélection d'un pion avec la souris.

 


 

Avant de commencer le développement, il convient d'aborder les principales différences entre les deux versions du framework boardifier.

Pour la partie modèle, les classes sont identiques à l'exception de la méthode update() de GameElement dont les paramètres changent : update(double width, double height, GridGeometry gridGeometry).  La valeur de ces paramètres est directement fournit par le contrôleur général, lorsque le modèle est mis à jour à chaque frame. Ces 3 paramètres correspondent à la taille en pixel de l'élément, et un objet donnant la géométrie visuelle de la grille (taille des cellules en pixel notamment) si l'élément est dans une grille. Normalement, en MVC le modèle n'a pas accès aux informations relatives au visuel mais il y a des exceptions. Par exemple, s'il faut déplacer un élément dans l'espace, il est parfois nécessaire de connaître sa forme et/ou la forme de la grille dans laquelle il se trouve pour savoir si le déplacement est possible. Dans "The Hole", ce genre de cas n'existe pas, mais comme boardifier est fait pour être général, ces exceptions sont prévues.

 

Pour la partie vue, presque toutes les classes sont différentes puisque c'est Javafx qui est utilisé pour faire le rendu visuel. En revanche, les principes généraux de création du visuel ne changent pas beaucoup par rapport à boardifier-console :

  • La vue générale est représentée par la classe View, que l'on va instancier dans la classe principale (celle de main() ), en fournissant le modèle en paramètre. On peut cependant créer une sous-classe, afin de définir une barre de menu spécifique pour le jeu à implémenter.
  • Le "panneau" principal de dessin, sur lequel vont être placés les éléments visuels (les looks) est représenté par la classe RootPane, qui hérite de la classe Javafx Pane. Le contenu par défaut du RootPane est un fond gris et un texte basique, qui peuvent servir de panneau d'introduction, avant de lancer une partie. Si l'on veut changer ce contenu par défaut, on crée une sous-classe et on redéfinit la méthode createDefaultGroup()​ (cf. ci-dessous).
  • Les looks sont définis grâce aux sous-classes existants de ElementLook, ou bien en en héritant. La différence est que les looks sont créés grâce aux classes Javafx telles que Rectangle, Circle, Text, ...
  • Pour chaque stage du jeu, on crée une sous-classes de GameStageView, afin de créer et stocker les éléments visuels (les looks) du stage. La différence est qu'il faut créer des looks avec des tailles en pixels au lieu de caractères.

 Pour la partie contrôle, il y a également des différences notables avec le mode console, notamment afin de gérer les mouvements des éléments graphiques et les interactions utilisateur grâce au clavier/souris. De ce fait, on a :

  • un contrôleur de l'animation : c'est lui qui va contrôler le temps qui passe et qui va régulièrement demander au contrôleur global de "mettre à jour" le jeu. Ce contrôleur n'a jamais besoin d'être redéfini.
  • un contrôleur du clavier : par défaut il ne fait rien, mais on peut créer une sous-classe pour détecter l'appui sur certaines touches.
  • un contrôleur de souris : par défaut il ne fait rien, mais on peut créer une sous-classe pour faire des actions en fonction de clic ou déplacement souris.
  • un contrôleur des actions : par défaut il ne fait rien, mais on peut créer une sous-classe pour faire des actions lorsqu'un élément graphique de javafx (bouton, case à cocher, menu, ...) est cliqué.
  • un contrôleur global : il contient presque tout ce qui est nécessaire pour mettre à jour le jeu, mais il faut obligatoirement créer une sous-classe pour pouvoir instancier les sous-contrôleurs énoncés ci-dessus, et définir ce qui se passe quand un joueur a fini son tour, ou bien quand un stage est terminé.

 

La dernière différence concerne les animations et la classe ActionPlayer. Elle est différente du mode console, puisqu'elle ajoute la possibilité de "jouer" des animations à l'écran, pour représenter une action de jeu. 

La super classe (dans le modèle) représentant les animations est Animation mais ce sont ses sous-classes qui sont réellement utilisables.

ATTENTION : Animation et ses sous-classes ne contiennent aucune instruction permettant de modifier directement la vue. Ces classes servent uniquement à stocker des informations sous la forme d'objets AnimationStep, qui seront utilisés pour modifier l'état d'un élément, par exemple sa position dans l'espace. Comme dit auparavant, la modification de l'état de l'élément entraînera une mise à jour automatique de son visuel, ce qui produira un effet visuel d'animation, par exemple de mouvement.

 

Animation ne contient que ce qui est nécessaire à toutes les animations, c.a.d des attributs représentant la durée de l'animation, une liste d'objets AnimationStep contenant les "pas" de l'animation, et un callback qui sera appelé à la fin de l'animation (par défaut ne fait rien).

AnimationStep est une simple "enveloppe" pour une liste de nombres à virgule. Les valeurs que l'on met dans cette liste dépend entièrement de ce dont on a besoin comme information pour représenter l'animation. Par exemple, pour une animation du type mouvement, cette liste va contenir les coordonnées x,y de l’élément à déplacer à un pas donné le l'animation. Si on veut faire changer l'apparence d'un élément, la liste va plutôt contenir l'indice d'une des apparences possible de l'élément (cf. DiceElement).

A l'heure actuelle, gamifier ne propose qu'un nombre limité d'animation, basées sur un changement soit de position, soit d'apparence. Il y a ainsi :

  • MoveAnimation : prend en paramètre des coordonnées de départ et d'arrivée dans l'espace. La liste d'AnimationStep ne contiendra que 2 pas, avec ces coordonnées. Cela provoque une animation de type téléportation.
  • LinearMoveAnimation  : idem que MoveAnimation mais en spécifiant un vitesse ou un temps de déplacement. La liste d'AnimationStep est remplie avec autant de pas que nécessaire pour respecter le temps ou la vitesse. Chaque pas contient des coordonnées dans l'espace, qui sont sur la ligne reliant le point d'arrivée et de départ.
  • FaceAnimation : prend en paramètre la liste des indices d'apparence que doit prendre l'élément., ainsi que le temps en ms entre chaque changement d'indice.
  • CyclicFaceAnimation : idem que FaceAnimation mais en faisant une boucle dont la longueur est donnée en paramètre.

 

ATTENTION : on ne crée généralement pas directement une instance de ces classes. On utilise plutôt un objet Action qui va contenir une Animation représentant l'action. Ce sera ActionPlayer qui lancera automatiquement l'animation. Par exemple, lorsqu'un pion doit se déplacer d'une case à une autre, on peut associer à une action de jeu instance de MoveAction une animation de type mouvement linéaire LinearMoveAnimation

 

 


 

Du fait de la présence d'une interface graphique, il y a plus de besoins que dans le jeu en mode console. Après discussion avec nous-même, on établit le scénario suivant :

  • lancement de l'application => la fenêtre principale n'affiche qu'un panneau d'introduction,
  • lancement d'un partie, avec un joueur humain contre un joueur ordinateur,
  • fin de partie => affichage d'une boite de dialogue pour dire qui est le gagnant, et qui permet de quitter le jeu ou débuter d'une nouvelle partie.

Parallèlement, on choisit de maquetter le jeu comme suivant :

  • la fenêtre principale contient une barre de menu, avec un unique menu contenant les items : "Start game", "Introduction", "Exit".
  • le panneau d'introduction contient seulement le texte "The Hole".
  • la boite de dialogue de fin de partie contient un texte signalant qui gagne ou bien si la partie est nulle, ainsi que 2 boutons intitulés "Start Game" et "Exit"
  • le panneau de jeu est globalement divisé comme ci-dessous :
 nom du joueur courant liste pions noirs 
 grille 3x3 liste pions rouge

 

Le scénario d'une partie est le suivant :

  • le joueur humain est le premier joueur et reçoit les pions noirs, l'ordinateur les rouges.
  • lorsque c'est le tour de l'humain, il doit d'abord sélectionner un de ses pions en cliquant dessus. Le pion change d'apparence pour signaler qu'il est sélectionné.
  • si ce pion peut être placé, les cases possibles changent d'apparence.
  • si un pion est sélectionné et que le joueur re-clique dessus, il est désélectionné. Si c'est un autre pion, il ne se passe rien.
  • si un pion est sélectionné et que le joueur clique sur une case possible, le pion est déplacé dans cette case, via une animation graphique.
  • c'est ensuite au joueur ordinateur de jouer, qui décider quel pion jouer et où le mettre. La sélection de ce pion n'est pas visible. Seule l'animation de déplacement doit être faite.

A partir de ces élément, on peut établir une liste synthétique des fonctionnalités et de sous-fonctionnalités dont elles dépendent :

  • lancer une partie,
  • quitter le jeu,
  • revenir au panneau d'introduction,
  • construire le visuel du panneau d'introduction, du panneau de jeu, du dialogue final,
  • sélectionner un pion :
    • trouver la liste des éléments de jeu contenant les coordonnées d'un clic souris,
    • déterminer la liste des cases possibles pour ce pion,
    • faire changer l'apparence d'un pion pour indiquer qu'il est (dé)sélectionné.
    • faire changer l'apparence d'une case de la grille pour indiquer que c'est une case de destination (im)possible.
  • désélectionner un pion :
    • faire changer l'apparence d'un pour indiquer qu'il est (dé)sélectionné.
    • faire changer l'apparence d'une case de la grille pour indiquer que c'est une case de destination (im)possible.
  • mettre un pion dans une case possible :
    • trouver la liste des éléments de jeu contenant les coordonnées d'un clic souris,
    • trouver dans la grille quelle case est cliquée,
    • lancer une animation qui déplace visuellement le pion vers sa case de destination.
  • calculer un déplacement de pion (pour l'ordinateur)
  • détecter la fin de partie,
  • calculer le résultat de la partie.

 

Nous allons voir que grâce à Boardifier, l'essentiel de ces fonctionnalités sont écrites, soit totalement, soit presque entièrement. Qui plus est, si on réutilise le code du jeu en mode console, les trois dernières sont également déjà écrites.

 


 

  • Le plus simple est d'utiliser la solution du jeu en mode console comme base.
  • Il suffit de copier les sources de cette solution dans un nouveau répertoire, puis de remplacer le répertoire boardifier avec le contenu de l'archive du framework boardifier : [ télécharger ]

Exemple (en supposant que les sources du jeu en mode console se trouvant dans ~/code/Java/HoleConsole) :

cd ~/code/Java
mkdir Hole
cd Hole
cp -r ../HoleConsole/src .
cd src
rm -rf boardifier
// télécharger l'archive boardifier.tgz et la placer dans le rep. courant.
tar zxf boardifier.tgz

 

  • Il suffit ensuite de créer un projet Idea, comme indiqué dans les tutoriels.

 


 

Les modifications des classes du modèle sont relativement limitées et concernent seulement 3 classes :

  • HoleStageFactory : modifier les coordonnées des éléments en pixels, et créer un TextElement pour représenter le nom du joueur courant dans la scène (contrainte non présente dans la version console)
  • HoleStageModel : ajouter tout ce qui concerne le TextElement (attribut +  getter/setter), et ce qui concerne la sélection d'un pion (état de la sélection & définition du callback quand on sélectionne un pion)

Remarque : pour fournir à boardifier le callback utilisé lors de la sélection d'un pion, il suffit d'appeler la méthode onSelectionChange() dans setupCallbacks() (même principe qu'en mode console avec onPutInGrid() ). Cette méthode prend en paramètre le callback, sous la forme d'une fonction lambda. Dans le cas présent, il suffit de vérifier si un pion est sélectionné. Si oui, on calcule les case atteignables en fonction du pion sélectionné, et sinon on remet à zéro les cases atteignables :

       onSelectionChange( () -> {
            // get the selected pawn if any
            if (selected.size() == 0) {
                board.resetReachableCells(false);
                return;
            }
            Pawn pawn = (Pawn) selected.get(0);
            board.setValidCells(pawn.getNumber());
        });
  • Pawn : redéfinir la méthode update() afin de jouer l'animation si elle existe.

Remarque : dans un jeu où les seules animations consistent à bouger des éléments (pions, cubes, ...), on peut utiliser le code "universel" suivant pour jouer une animation de déplacement linéaire :

    public void update(double width, double height, GridGeometry gridGeometry) {
        // if must be animated, move the pawn
        if (animation != null) {
            AnimationStep step = animation.next();
            if (step != null) {
                setLocation(step.getInt(0), step.getInt(1));
            }
            else {
                animation = null;
            }
        }
    }

 

Toutes ces modifications sont relativement simples à mettre en place et se retrouvent dans la solution téléchargeable en fin d'article.

  


 

La prise de décision se fait comme en mode console, dans la classe HoleDecider. La seule différence est que l'on peut cette fois utiliser le constructeur des actions avec une animation en paramètre. Le seul problème est de déterminer les coordonnées d'arrivée du pion dans le plan. Pour cela, il suffit d'utiliser la méthode "helper" getRootPaneLocationForCellCenter(), accessible via le GridLook de la grille dans laquelle le pion va être placé, comme dans l'exemple suivant :

 

// do a cast get a variable of the real type to get access to the attributes of HoleStageModel
HoleStageModel stage = (HoleStageModel)model.getGameStage();
HoleBoard board = stage.getBoard(); // get the board
...
// create action list. After the last action, it is next player's turn.
ActionList actions = new ActionList(true);
// get the dest. cell center in space.
GridLook look = (GridLook) control.getElementLook(board);
Coord2D center = look.getRootPaneLocationForCellCenter(rowDest, colDest);
// create the move action
GameAction move = new MoveAction(model, pawn, "holeboard", rowDest, colDest, AnimationTypes.MOVE_LINEARPROP, center.getX(), center.getY(), 10);
actions.addSingleAction(move);

   


 

En JavaFx, lors du lancement d'un application, une instance de la classe Stage est créée. Attention : cette classe n'a rien à voir avec la notion de stage dans boardifier. Elle représente en fait la fenêtre primaire de l'application. Si l'on veut ouvrir d'autres fenêtres, il faut donc créer d'autres instance de Stage. Ensuite, pour afficher quelque chose au sein de cette fenêtre, il faut créer une instance de Scene et lui ajouter des éléments graphiques JavaFx (Button, Shape, ...). On peut alors assigner la Scene au Stage et la machinerie interne de JavaFx se débrouille pour effectivement peindre les éléments graphiques à l'écran, et si besoin les repeindre quand leurs caractéristiques (position, dimension ,...) ont changé.

En conclusion, contrairement à Swing, il n'y a pas besoin d'utiliser des instructions pour "peindre" des formes, comme par exemple drawRect(), puisqu'il y a des classes qui représentent ces formes. De plus, il est inutile de demander explicitement à repeindre un élément avec une méthode du style repaint(). JavaFx est donc plus souple à utiliser ... une fois qu'on connaît bien ses classes et leurs interactions !


 Pour créer le visuel des éléments, donc une sous-classe de ElementLook, boardifier repose sur les principes suivants (certains étant communs avec boardifier-console):

  • une sous-classe d'ElementLook prend forcément en paramètre un GameElement (= principe MVC où la vue accède au modèle)
  • le visuel est compris dans une boîte englobante, dont le coin haut-gauche se trouve aux coordonnées virtuelles 0,0.
  • les dimensions de la boîte englobante ne sont pas forcément fixes et peuvent dépendre des paramètres passés au constructeur, ou bien venant du GameElement associé.
  • on crée les différentes parties du visuel grâce à des instances de sous-classes de Shape (Circle, Rectangle, Line, ...) et/ou ImageView (pour afficher une image).
  • on positionne ces instances dans la boîte englobante, en utilisant leurs méthodes propres (par ex, setCenterX() pour Circle)
  • on "ajoute" les instances au look grâce à
    • addShape() pour les sous-classes de Shape,
    • addNode() pour une ImageView.
  • si besoin, on redéfinit les méthodes :
    • onSelectionChange(), car le visuel doit changer lorsqu'on l'élément associé est (dé)sélectionné,
    • onChange(), car le visuel doit changer lors d'un changement d'état (autre que position, visibilité, sélection) de l'élément associé.

Attention :

  • si vous n'ajoutez pas l'instance au look avec addShape() ou addNode(), elle ne sera jamais visible
  • si la position d'une instance n'est pas correcte, ce qui dépasse de la boîte englobante ne sera pas coupé. En revanche, il deviendra difficile de positionner correctement à l'écran l'élément.

 

Par exemple, pour créer le look des pions de "The Hole", PawnLook contient (entre autre) :

...
public class PawnLook extends ElementLook {
    private Circle circle;
    public PawnLook(int radius, GameElement element) {
        super(element);
        Pawn pawn = (Pawn)element; // unless you did somthing stupid in HoleStageView, it IS really a Pawn instance !
        circle = new Circle();
        circle.setRadius(radius);
        if (pawn.getColor() == Pawn.PAWN_BLACK) {
            circle.setFill(Color.BLACK);
        }
        else {
            circle.setFill(Color.RED);
        }
        circle.setCenterX(radius);
        circle.setCenterY(radius);
        // add the circle to the look
        addShape(circle);
        // to fulfill ...
    }
    @Override
    public void onSelectionChange() {
        // to fulfill ...
    } 
    public void onChange() { }
}
 

Remarque : il n'y a pas besoin définir du code pour onChange() car il n'y a aucun changement d'état dans Pawn (à part la position et la sélection). Ce ne serait pas le cas dans un jeu de dames où un pion peut devenir une dame. Il faudrait alors redéfinir onChange() pour modifier son visuel quand le pion devient un reine.


Comme en mode console, tous les looks doivent être instanciés dans la fonction createLooks() d'une sous-classe de GameStageView. Comme dit plus haut, la différence est que les dimensions des looks doivent être données en pixels. Si le jeu comporte plusieurs stages, il faut créer une sous-classe de GameStageView par stage. Dans le cas de "The Hole", il n'y a qu'une seule classe HoleStageView, qui ressemble fortement à celle du mode console :

...
public class HoleStageView extends GameStageView {
    public HoleStageView(String name, GameStageModel gameStageModel) {
        super(name, gameStageModel);
        width = 650;
        height = 450;
    }

    @Override
    public void createLooks() {
        HoleStageModel model = (HoleStageModel)gameStageModel;

        addLook(new HoleBoardLook(320, model.getBoard()));
        addLook(new PawnPotLook(120,420,model.getBlackPot()));
        ... // to fulfill        
}

 


Tous les looks stockés dans les différentes instances de GameStageView vont être affichés dans un Pane, classe JavaFx servant d'espace d'affichage pour les éléments javafx (Shape, ImageView, ...). Ce Pane est représenté par la classe RootPane de boardifier (qui hérite de Pane). Au moment de son instanciation, RootPane contient seulement un rectangle gris et un texte basique. Si on veut changer cet état par défaut, il faut hérité de RootPane, ce qui est la cas dans "The Hole", avec la classe HoleRootPane, qui redéfinit la méthode createDefaultGroup() comme suivant :

...
public class HoleRootPane extends RootPane {
    ...
    @Override
    public void createDefaultGroup() {
        Rectangle frame = new Rectangle(600, 100, Color.LIGHTGREY);
        Text text = new Text("Playing to The Hole");
        text.setFont(new Font(15));
        text.setFill(Color.BLACK);
        text.setX(10);
        text.setY(50);
        // put shapes in the group
        group.getChildren().clear();
        group.getChildren().addAll(frame, text);
    }
}

 


Enfin, la vue globale est représentée par la classe View, dont il faut hériter si l'on a besoin d'une barre de menu. Dans ce cas, il faut redéfinir la méthode createMenuBar() :

...
public class HoleView extends View {

    private MenuItem menuStart;
    private MenuItem menuIntro;
    private MenuItem menuQuit;

    public HoleView(Model model, Stage stage, RootPane rootPane) {
        super(model, stage, rootPane);
    }

    @Override
    protected void createMenuBar() {
        menuBar = new MenuBar();
        Menu menu1 = new Menu("Game");
        menuStart = new MenuItem("New game");
        menuIntro = new MenuItem("Intro");
        menuQuit = new MenuItem("Quit");
        menu1.getItems().add(menuStart);
        menu1.getItems().add(menuIntro);
        menu1.getItems().add(menuQuit);
        menuBar.getMenus().add(menu1);
    }
    ...
}

  

Cette partie est celle qui nécessite le plus de modifications, et globalement les plus compliquées à écrire.Pour un jeu en général, il faut créer :

  • une sous-classe de ControllerKey, pour gérer les interactions clavier,
  • une sous-classe de ControllerMouse, pour gérer les interactions souris,
  • une sous-classe de ControllerAction, pour gérer la barre de menu
  • une sous-classe de Controller, pour créer le contrôleur global, et notamment implémenter l'alternance entre joueurs.

 

NB : Dans "The Hole",  il faut donc créer :

  • HoleControllerKey, qui ne sert à rien dans le jeu mais permet d'illustrer les événements clavier. 
  • HoleControllerMouse, qui va s'occuper de détecter le clic souris et de voir si un pion a été sélectionné et si ou, si une case d'arrivée est cliquée.
  • HoleControllerAction, pour gérer le clic sur les items du menu.
  • HoleController.

 

Pour HoleCntroller, à part le constructeur qui doit instancier les sous-contrôleurs, il n'existe qu'une seul méthode à redéfinir : nextPlayer(), qui contient le code a exécuter lorsque le tour d'un joueur vient de se terminer. Dans le cas présent, que ce soit le joueur humain ou l'ordinateur, les actions de jeu vont être encodées dans une ActionList jouée par un ActionPlayer. Si on crée l'ActionList avec son attribut doNextPlayer à true, l'ActionPlayer appellera automatiquement nextPlayer() une fois que toutes les actions auront été jouées. Cela permettra donc d'alterner automatiquement entre les joueurs.

A noter qu'il est également possible d'appeler cette méthode dans le contrôleur de souris ou clavier, par exemple lorsque l'on clique sur un élément précis, ou bien lors de l'appui sur une touche fixée. 

Pour "The Hole", le code de nextPlayer() est très simple :

  • modifier le joueur courant dans le modèle, ce qui peut être fait grâce à model.setNextPlayer() qui va par défaut boucler sur tous les joueurs,
  • changer le texte du TextElement qui contient le nom du joueur courant,
  • si le joueur courant est l'ordinateur, créer un HoleDecider, le passer en paramètre de construction à un ActionPlayer et enfin démarrer l'ActionPlayer.

Cela donne :

public void nextPlayer() {
    // use the default method to compute next player
    model.setNextPlayer();
    // get the new player
    Player p = model.getCurrentPlayer();
    // change the text of the TextElement
    HoleStageModel stageModel = (HoleStageModel) model.getGameStage();
    stageModel.getPlayerName().setText(p.getName());
    if (p.getType() == Player.COMPUTER) {
        System.out.println("COMPUTER PLAYS");
        HoleDecider decider = new HoleDecider(model,this);
        ActionPlayer play = new ActionPlayer(model, this, decider, null);
        play.start();
    }
}

 

Pour HoleControllerMouse, il faut compléter la méthode handle() qui sert de gestionnaire des événements souris. C'est une des parties du jeu les plus complexes à écrire du jeu car elle demande de bien connaître boardifier. En revanche, d'un point de vue algorithmique, c'est assez simple car le scénario d'utilisation du jeu est lui-même simple. Cela donne :

  • récupérer les coordonnées du clic souris dans la scène,
  • obtenir la liste des GameElement contenant ces coordonnées,
  • si aucun pion n'est sélectionné :
    • parcourir la liste pour trouver un élément du type pion. Si aucun, retour.
    • sinon, si la couleur du pion correspond au joueur courant, sélectionner le pion et retour.
  • sinon :
    • parcourir la liste pour trouver un élément du type pion. Si trouvé et qu'il s'agit du pion déjà sélectionné, désélectionner le pion puis retour.
    • parcourir la liste pour trouver le panneau 3x3. Si pas trouvé, retour.
    • sinon 
      • trouver la case cliquée
      • si case non valide, retour
      • déterminer le centre de la case cliquée.
      • créer une ActionList.
      • insérer une MoveAction dans l'ActionList, avec comme coordonnées finale le centre de la case cliquée.
      • créer une ActionPlayer et jouer l'ActionList.
      • désélectionner le pion.

Le code complet est donnée dans la solution complète en fin d'article.

 

Dans HoleControllerAction, il faut mettre en place la détection des clics sur un item de menu, ce qui génère un ActionEvent, et en fonction de l'item cliqué, exécuter certains instructions. Il existe plusieurs façon de mettre en place cela. Dans la solution proposée, on n'utilise pas la méthode handle() comme dans le cas du clavier ou de la souris. En effet, comme les instructions à exécuter en cas de clic sur un menu sont relativement simples et concises, il est possible d'attacher un callback à un item de menu, grâce à la méthode setOnAction(). Le paramètre de cette méthode est une fonction lambda contenant les instructions a exécuter en cas de clic. Cette lambda prend en paramètre l'ActionEvent généré lors du clic, si besoin.

HoleControllerAction étant un contrôleur, il a accès à la vue, donc peut accéder aux items de menus via les getters définis dans HoleView, et ainsi définir les callback à appeler avec setOnAction(). Exemple partiel, tiré de la solution proposée ;

...
public class HoleControllerAction extends ControllerAction implements EventHandler<ActionEvent> {
    // to avoid lots of casts, create an attribute that matches the instance type.
    private HoleView holeView;

    public HoleControllerAction(Model model, View view, Controller control) {
        super(model, view, control);
        // take the view parameter to define a local view attribute with the real instance type, i.e. HoleView.
        holeView = (HoleView) view;
        // set handlers dedicated to menu items
        setMenuHandlers();
        ...
    }

    private void setMenuHandlers() {
        ...
        // set event handler on the MenuQuit item
        holeView.getMenuQuit().setOnAction(e -> {
            System.exit(0);
        });
    }
    ...
}

 

Remarque : HoleControllerAction prend en paramètre un objet View, mais lors de l'exécution, on sait que cet objet sera en réalité une instance de HoleView. Pour accéder aux getters de HoleView, il faut donc transtyper le paramètre view. Pour éviter de faire ce transtypage à chaque utilisation, il est fait une seule fois pour un attribut du bon type, c.a.d. HoleView.

 


 

La classe Hole ressemble fortement à celle du mode console. Cette classe doit forcément hériter de Application. Dans main(), il suffit d'analyser les paramètres pour vérifier s'ils sont corrects puis appeler la méthode launch() qui lance le "moteur" javafx. Ce dernier va automatiquement appeler la méthode start(), qu'il faut implémenter avec les étapes suivantes :

  • créer le modèle global,
  • ajouter des joueurs au modèle,
  • enregistrer des noms des classes associées aux stages dans le StageFactory,
  • créer le panneau principal, dans le cas présent, une instance de HoleRootPane.
  • créer la vue globale,
  • créer le contrôleur global,
  • définir le premier stage,
  • afficher la fenêtre principale.

 


 

 L'archive de la solution complète est téléchargeable [ ici ].