Index de l'article

  

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.