IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Programmation Java pour les enfants, les parents et les grands-parents


précédentsommairesuivant

14. Chapitre 11. Retour sur le graphique - le jeu de ping-pong

Dans les chapitres 5, 6 et 7, nous avons utilisé certains composants AWT et Swing. Je vais maintenant te montrer comment dessiner et déplacer dans une fenêtre des objets tels que des ovales, des rectangles et des lignes. Nous verrons aussi comment traiter les événements de la souris et du clavier. Pour rendre plus amusant ces sujets ennuyeux, nous allons explorer toutes ces choses en créant un jeu de ping-pong. Il y aura deux joueurs, que j'appelle l'enfant et l'ordinateur.

14-A. Stratégie

Commençons par les règles du jeu :

  1. La partie se poursuit jusqu'à ce que l'un des joueurs (l'enfant ou l'ordinateur) atteigne le score de 21.
  2. Les mouvements de la raquette de l'enfant sont contrôlés par la souris.
  3. Le score est affiché en bas de la fenêtre.
  4. Une nouvelle partie commence quand un joueur appuie sur la touche N du clavier. Q met fin à la partie. S effectue le service.
  5. Seul l'enfant peut servir.
  6. Pour gagner un point, la balle doit dépasser la ligne à la verticale de la raquette (sans avoir été bloquée par la raquette).
  7. Quand l'ordinateur renvoie la balle, elle ne peut se déplacer qu'horizontalement vers la droite.
  8. Si la balle rencontre la raquette de l'enfant dans la moitié supérieure de la table, elle doit se déplacer dans la direction haut-gauche. Si la balle se trouve dans la partie inférieure de la table, elle doit se déplacer dans la direction bas-gauche.

Tu dois penser que ça va être difficile à programmer. L'astuce consiste à découper une tâche complexe en un ensemble de tâches plus petites et plus simples, puis d'essayer de les résoudre une par une.

Cette méthode de travail est appelée réflexion analytique (analytical thinking). Elle est utile non seulement en programmation, mais aussi dans la vie en général : ne sois pas frustré si tu ne peux pas atteindre un objectif important, mais découpe-le en un ensemble d'objectifs plus petits que tu peux atteindre un par un !

C'est pour cette raison que la première version du jeu n'implante que quelques-unes de ces règles : elle dessine la table, déplace la raquette et affiche les coordonnées du pointeur de la souris quand on clique.

Au lieu de te contenter de dire « mon ordinateur ne fonctionne pas » (vaste problème), essaye de voir ce qui ne va pas exactement (trouve un problème plus petit). 1. L'ordinateur est-il branché au secteur (oui/non) ? Oui. 2. Quand je mets en route l'ordinateur, l'écran avec toutes ses icônes est-il affiché (oui/non) ? Oui. 3. Puis-je déplacer le pointeur de la souris sur l'écran (oui/non) ? Non. 4. Le câble de la souris est-il branché correctement (oui/non) ? Non. Il suffit de brancher la souris et l'ordinateur fonctionnera à nouveau ! Un gros problème a été réduit à la simple correction du branchement du câble de la souris.

14-B. Code

Ce jeu est constitué des trois classes suivantes :

  • La classe TableVertePingPong se charge de la partie visuelle. Pendant la partie, elle affiche la table, les raquettes et la balle.
  • La classe MoteurJeuPingPong est responsable du calcul des coordonnées de la balle et des raquettes, du démarrage et de l'arrêt de la partie, et du service. Cette classe passe les coordonnées des composants à la classe TableVertePingPong, qui rafraîchit son affichage en fonction de celles-ci.
  • L'interface ConstantesDuJeu contient les déclarations de toutes les constantes nécessaires au jeu, telles que la largeur et la hauteur de la table, les positions de départ des raquettes, etc..

La table de ping-pong ressemble à ceci :

Image non disponible

La première version de ce jeu ne fait que trois choses :

  • Afficher une table de ping pong verte.
  • Afficher les coordonnées du pointeur de la souris quand on clique.
  • Déplacer la raquette de l'enfant vers le haut et vers le bas.

Le code de notre classe TableVertePingPong, qui est une sous-classe de la classe Swing JPanel, se trouve deux pages plus bas. Regarde-le pendant que tu lis le texte ci-dessous.

Puisque notre jeu a besoin de connaître les coordonnées exactes du pointeur de la souris, le constructeur de la classe TableVertePingPong crée une instance du récepteur d'événements MoteurJeuPingPong. Cette classe effectue certaines actions quand l'enfant clique ou simplement déplace la souris.

La méthode ajouteAuCadre() crée un libellé qui affichera les coordonnées de la souris.

Cette classe n'est pas une applet, raison pour laquelle elle utilise la méthode paintComponent() au lieu de la méthode paint(). Cette méthode est appelée soit par Java quand il est nécessaire de rafraîchir la fenêtre, soit quand notre programme appelle la méthode repaint(). Tu as bien lu, la méthode repaint() appelle en interne la méthode paintComponent() et fournit à ta classe un objet Graphics pour que tu puisses dessiner dans la fenêtre. Nous appellerons cette méthode à chaque fois que nous aurons recalculé les coordonnées des raquettes ou de la balle pour les afficher au bon endroit.

Pour dessiner une raquette, il faut lui affecter une couleur, puis remplir un rectangle avec cette peinture à l'aide de la méthode fillRect() (remplir rectangle). Cette méthode doit connaître les coordonnées X et Y du coin haut gauche du rectangle, ainsi que sa largeur et sa hauteur en pixels. La balle est dessinée à l'aide de la méthode fillOval() (remplir ovale), qui a besoin de connaître les coordonnées du centre de l'ovale, sa hauteur et sa largeur. Si la hauteur et la largeur de l'ovale sont identiques, c'est un cercle.

Dans une fenêtre, la coordonnée X augmente de la gauche vers la droite et la coordonnée Y augmente du haut vers le bas. Par exemple, les coordonnées des coins de ce rectangle de largeur 100 pixels et de hauteur 70 sont indiquées entre parenthèses :

Image non disponible

Une autre méthode intéressante est la méthode getPreferredSize(). Nous créons une instance de la classe Swing Dimension pour fixer la taille de la table. Java a besoin de connaître les dimensions de la fenêtre et appelle pour cela la méthode getPreferredSize() de l'objet TableVertePingPong. Cette méthode retourne à Java un objet Dimension que nous avons créé dans le code en fonction de la taille de notre table.

Les deux classes table et moteur utilisent des valeurs constantes, qui ne changent jamais. Par exemple, la classe TableVertePingPong utilise la largeur et la hauteur de la table et MoteurJeuPingPong doit connaître les incréments de déplacement de la balle (plus l'incrément est petit, plus le mouvement est fluide). Il est commode de réunir toutes les constantes (variables final) au sein d'une interface. Dans notre jeu, l'interface se nomme ConstantesDuJeu. Si une classe a besoin de ces valeurs, il suffit d'ajouter implements ConstantesDuJeu à la déclaration de la classe pour utiliser n'importe laquelle des variables final de cette interface comme si elle était déclarée dans la classe elle-même. C'est pourquoi nos deux classes table et moteur implantent l'interface ConstantesDuJeu.

Si tu décides de modifier la taille de la table, de la balle ou des raquettes, tu n'as besoin de le faire qu'à un endroit : dans l'interface ConstantesDuJeu. Examinons le code de la classe TableVertePingPong et de l'interface ConstantesDuJeu.

 
Sélectionnez
package écrans;

import javax.swing.JPanel;
import javax.swing.JFrame;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.WindowConstants;
import java.awt.Point;
import java.awt.Dimension;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Color;
import moteur.MoteurJeuPingPong;


/**
* Cette classe dessine une table de ping-pong verte 
* et affiche les coordonnées du point où l'utilisateur
* a cliqué. 
*/
public class TableVertePingPong 
    extends JPanel              
    implements ConstantesDuJeu {    

  JLabel label;
  public Point point = new Point(0,0); 
    
  public int raquetteOrdinateur_X = 15;
  private int raquetteEnfant_Y = RAQUETTE_ENFANT_Y_DEPART;
    
  Dimension taillePréférée = new 
                     Dimension(LARGEUR_TABLE, HAUTEUR_TABLE);
    
  // Cette méthode affecte sa taille au cadre.
  // Elle est appelée par Java.
  public Dimension getPreferredSize() {
    return taillePréférée;
  }
  
  // Constructeur. 
  TableVertePingPong() {
 
    MoteurJeuPingPong moteurJeu =
                                 new MoteurJeuPingPong(this);
    // Reçoit les clics pour l'affichage de leurs coordonnées 
    addMouseListener(moteurJeu);
    // Reçoit les mouvements de la souris pour 
    // le déplacement des raquettes
    addMouseMotionListener(moteurJeu);
  }

  // Ajoute à une fenêtre un panneau contenant cette table et 
  // un JLabel
  void ajouteAuCadre(Container conteneur) {
    conteneur.setLayout(new BoxLayout(conteneur,
                                      BoxLayout.Y_AXIS));
    conteneur.add(this);
    label = new JLabel("Coordonnées...");
    conteneur.add(label);
  }

  // Repeint la fenêtre. Cette méthode est appelée par Java 
  // quand il est nécessaire de rafraîchir l'écran ou quand 
  // la méthode repaint() est appelée par le 
  // MoteurJeuPingPong.
  public void paintComponent(Graphics contexteGraphique) {
    super.paintComponent(contexteGraphique);  

    // Dessine la table verte
    contexteGraphique.setColor(Color.GREEN);
    contexteGraphique.fillRect(
                      0, 0, LARGEUR_TABLE, HAUTEUR_TABLE); 
        
    // Dessine la raquette droite  
    contexteGraphique.setColor(Color.yellow);
    contexteGraphique.fillRect(RAQUETTE_ENFANT_X_DEPART,
                               raquetteEnfant_Y, 5, 30); 

    // Dessine la raquette gauche
    contexteGraphique.setColor(Color.blue);
    contexteGraphique.fillRect(
                      raquetteOrdinateur_X, 100, 5, 30); 
        
    // Dessine la balle
    contexteGraphique.setColor(Color.red);
    contexteGraphique.fillOval(25, 110, 10, 10); 
        
    // Dessine les lignes
    contexteGraphique.setColor(Color.white);
    contexteGraphique.drawRect(10, 10, 300, 200);
    contexteGraphique.drawLine(160, 10, 160, 210);
    
    // Affiche un point sous forme de rectangle de 2x2 pixels 
    if (point != null) {
      label.setText("Coordonnées (x, y) : " + 
                      point.x + ", " + point.y);
        contexteGraphique.fillRect(point.x, point.y, 2, 2);
    }
  }
    
  // Affecte sa position courante à la raquette de l'enfant
  public void positionnerRaquetteEnfant_Y(int y) {
    this.raquetteEnfant_Y = y;
  }
    
  // Retourne la position courante de la raquette de l'enfant
  public int coordonnéeRaquetteEnfant_Y() {
    return raquetteEnfant_Y;
  }

  public static void main(String[] args) {
    // Crée une instance du cadre
    JFrame monCadre = new JFrame("Table verte de ping-pong");
    // Permet la fermeture de la fenêtre par clic sur la 
    // petite croix dans le coin.
    monCadre.setDefaultCloseOperation(
                 WindowConstants.EXIT_ON_CLOSE);
        
    TableVertePingPong table = new TableVertePingPong();
    table.ajouteAuCadre(monCadre.getContentPane());
    // Affecte sa taille au cadre et le rend visible.
    monCadre.pack();
    monCadre.setVisible(true);
  }
}

Voici l'interface ConstantesDuJeu. Toutes les valeurs des variables sont en pixels. Écris les noms des variables final en lettres majuscules.

 
Sélectionnez
package écrans;

public interface ConstantesDuJeu {
  public final int LARGEUR_TABLE = 320; 
  public final int HAUTEUR_TABLE = 220;
  public final int RAQUETTE_ENFANT_Y_DEPART = 100;
  public final int RAQUETTE_ENFANT_X_DEPART = 300;
  public final int HAUT_TABLE = 12;
  public final int BAS_TABLE = 180;
    
  public final int INCREMENT_RAQUETTE = 4;
}

Un programme en cours d'exécution ne peut pas modifier les valeurs de ces variables, puisqu'elles sont déclarées comme final. Mais si, par exemple, tu décides d'augmenter la taille de la table, tu dois modifier les valeurs de LARGEUR_TABLE et de HAUTEUR_TABLE, puis recompiler l'interface ConstantesDuJeu.

Dans ce jeu, les décisions sont prises par la classe MoteurJeuPingPong, qui implante deux interfaces concernant la souris. MouseListener n'a de code que dans la méthode mousePressed(). À chaque clic, cette méthode dessine un point blanc sur la table et affiche ses coordonnées. En réalité, ce code n'est d'aucune utilité pour notre jeu, mais il te montre d'une façon simple comment obtenir les coordonnées de la souris de l'objet MouseEvent passé au programme par Java.

La méthode mousePressed() affecte les coordonnées de la variable point en fonction de la position du pointeur de la souris au moment où l'utilisateur a cliqué. Une fois les coordonnées affectées, elle demande à Java de repeindre la table.

MouseMotionListener répond aux mouvements de la souris au-dessus de la table. Nous utilisons sa méthode mouseMoved() pour déplacer la raquette de l'enfant vers le haut et vers le bas.

La méthode mouseMoved() calcule la position suivante de la raquette de l'enfant. Quand le pointeur de la souris se trouve au-dessus de la raquette (la coordonnée Y de la souris est inférieure à la coordonnée Y de la raquette), cette méthode assure que la raquette n'ira pas plus loin que le haut de la table.

Quand le constructeur de la table crée l'objet moteur, il lui passe une référence à l'instance de table (le mot-clé this représente une référence à l'emplacement en mémoire de l'objet TableVertePingPong). Le moteur peut maintenant « parler » à la table, par exemple pour affecter de nouvelles coordonnées à la balle ou pour repeindre la table si nécessaire. Si cette partie n'est pas claire, tu peux relire la section concernant le passage de données entre classes, au Chapitre 6.

Dans notre jeu, les raquettes se déplacent verticalement d'une position à une autre en utilisant un incrément de quatre pixels, comme le définit l'interface ConstantesDuJeu (la classe moteur implante cette interface). Par exemple, la ligne suivante soustrait quatre à la valeur de la variable raquetteEnfant_Y :

 
Sélectionnez
raquetteEnfant_Y -= INCREMENT_RAQUETTE;

Si la coordonnée Y de la raquette valait 100, elle devient 96 après cette ligne de code, ce qui signifie que la raquette doit être déplacée vers le haut. Tu obtiendrais le même résultat en utilisant la syntaxe suivante :

 
Sélectionnez
raquetteEnfant_Y = raquetteEnfant_Y - INCREMENT_RAQUETTE;

Souviens-toi, nous avons abordé les différents moyens de modifier la valeur d'une variable au Chapitre 3.

Voici la classe MoteurJeuPingPong.

 
Sélectionnez
package moteur;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import écrans.*;
public class MoteurJeuPingPong  implements  
       MouseListener, MouseMotionListener, ConstantesDuJeu {
        
  TableVertePingPong table;
  public int raquetteEnfant_Y = RAQUETTE_ENFANT_Y_DEPART;
  // Constructeur. Stocke une référence à la table.
  public MoteurJeuPingPong(TableVertePingPong tableVerte) {
    table = tableVerte;
  }
  // Méthodes requises par l'interface MouseListener.
  public void mousePressed(MouseEvent événement) {
    // Récupère les coordonnées X et Y du pointeur de la     
    // souris et les affecte au "point blanc" sur la table.
    table.point.x = événement.getX();
    table.point.y = événement.getY();
    // La méthode repaint appelle en interne la méthode
    // paintComponent() de la table qui rafraîchit la
    // fenêtre.  
    table.repaint();
  }
  public void mouseReleased(MouseEvent événement) {}
  public void mouseEntered(MouseEvent événement) {}
  public void mouseExited(MouseEvent événement) {}
  public void mouseClicked(MouseEvent événement) {}
    
  // Méthodes requises par l'interface MouseMotionListener.
  public void mouseDragged(MouseEvent événement) {}

  public void mouseMoved(MouseEvent événement) {
    int souris_Y = événement.getY();
    // Si la souris est au-dessus de la raquette de l'enfant
    // et que la raquette n'a pas dépassé la limite 
    // supérieure de la table, la déplace vers le haut ; 
    // sinon, la déplace vers le bas.            
    if (souris_Y < raquetteEnfant_Y 
                       && raquetteEnfant_Y > HAUT_TABLE) {
      raquetteEnfant_Y -= INCREMENT_RAQUETTE;
    } else if (raquetteEnfant_Y < BAS_TABLE) {
      raquetteEnfant_Y += INCREMENT_RAQUETTE;
    }
    // Affecte la nouvelle position de la raquette dans la 
    // classe table
    table.positionnerRaquetteEnfant_Y(raquetteEnfant_Y);
    table.repaint();        
  }
}

14-C. Bases des fils d'exécution Java

Jusqu'ici, tous nos programmes s'exécutent en séquence - une action après l'autre. Si un programme appelle deux méthodes, la seconde méthode attend que la première se soit terminée. Autrement dit, chacun de nos programmes n'a qu'un fil d'exécution (thread of execution).

Cependant, dans la vraie vie, nous pouvons faire plusieurs choses en même temps, comme manger, parler au téléphone, regarder la télévision et faire nos devoirs. Pour mener à bien toutes ces actions en parallèle, nous utilisons plusieurs processeurs : les mains, les yeux et la bouche.

Image non disponible

Certains des ordinateurs les plus chers ont aussi deux processeurs ou plus. Mais sans doute ton ordinateur n'a-t-il qu'un processeur qui effectue les calculs, envoie les commandes à l'écran, au disque, aux ordinateurs distants, etc.

Même un unique processeur peut exécuter plusieurs actions à la fois si le programme utilise des fils d'exécution multiples (multiple threads). Une classe Java peut lancer plusieurs fils d'exécution qui obtiennent chacun à leur tour des tranches du temps de l'ordinateur.

Un bon exemple de programme capable de créer de multiples fils d'exécution est un navigateur web. Tu peux naviguer sur Internet tout en téléchargeant des fichiers : un seul programme exécute deux fils d'exécution.

La version suivante de notre jeu de ping-pong a un fil d'exécution dédié à l'affichage de la table. Le second fil d'exécution calcule les coordonnées de la balle et des raquettes et envoie les commandes au premier fil d'exécution pour repeindre la fenêtre. Mais tout d'abord, je vais te montrer deux programmes très simples pour mieux te faire comprendre pourquoi les fils d'exécution sont nécessaires.

Chacun de ces programmes d'exemple affiche un bouton et un champ textuel.

Quand on appuie sur le bouton Tuer le temps, le programme entre dans une boucle qui incrémente une variable trente mille fois. La valeur courante de la variable compteur est affichée dans la barre de titre de la fenêtre. Image non disponible

La classe ExempleSansFils n'a qu'un fil d'exécution et il est impossible de saisir quelque chose dans le champ textuel tant que la boucle n'est pas terminée. Cette boucle accapare tout le temps de calcul du processeur, c'est pourquoi la fenêtre est verrouillée.

Compile et exécute cette classe et constate par toi-même que la fenêtre est verrouillée pendant un moment. Note que cette classe crée une instance de JTextField et la passe au contenu de la fenêtre sans déclarer de variable représentant cette instance. Si tu ne comptes pas lire ou modifier d'attributs de cet objet dans ton programme, tu n'as pas besoin de mémoriser une telle référence.

 
Sélectionnez
import javax.swing.*;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class ExempleSansFils extends JFrame
                            implements ActionListener {
  // Constructeur
  ExempleSansFils() {
    // Crée un cadre contenant un bouton et un champ textuel
    GridLayout disposition = new GridLayout(2,1); 
    this.getContentPane().setLayout(disposition);
    JButton monBouton = new JButton("Tuer le temps");
    monBouton.addActionListener(this);
    this.getContentPane().add(monBouton);    
    this.getContentPane().add(new JTextField());
  }
  // Traite les clics sur le bouton
  public void actionPerformed(ActionEvent événement) {
    // Tue juste un peu le temps pour montrer que
    // les contrôles de la fenêtre sont verrouillés.
    for (int i = 0; i < 30000; i++) {
      this.setTitle("i = " + i);
    }
  }
    
  public static void main(String[] args) {
    // Crée une instance du cadre
    ExempleSansFils maFenêtre = new ExempleSansFils();
    // Permet la fermeture de la fenêtre par clic sur la 
    // petite croix dans le coin.
    maFenêtre.setDefaultCloseOperation(
                      WindowConstants.EXIT_ON_CLOSE);
        
    // Affecte sa taille au cadre - coordonnées du coin haut 
    // gauche, largeur et hauteur.
    maFenêtre.setBounds(0, 0, 200, 100);
    // Rend la fenêtre visible.
    maFenêtre.setVisible(true);
  }
}

La version suivante de cette petite fenêtre crée et lance un fil d'exécution séparé pour la boucle ; le fil d'exécution principal de la fenêtre te permet de taper dans le champ textuel pendant que la boucle s'exécute.

En Java, on peut créer un fil d'exécution par l'un des moyens suivants :

1. Créer une instance de la classe Java Thread et lui passer un objet qui implante l'interface Runnable. Si cette classe implante l'interface Runnable, le code ressemble à ceci :

 
Sélectionnez
Thread travailleur = new Thread(this);

Cette interface t'impose d'écrire dans la méthode run() le code qui doit être exécuté comme un fil d'exécution séparé. Mais pour lancer le fil d'exécution, tu dois appeler la méthode start(), qui va en fait appeler ta méthode run(). D'accord, c'est un peu troublant, mais c'est comme ça que tu démarres le fil d'exécution :

 
Sélectionnez
travailleur.start();

2. Créer une sous-classe de la classe Thread et y implanter la méthode run(). Pour démarrer le fil d'exécution, appeler la méthode start().

 
Sélectionnez
public class MonFil extends Thread {

  public static void main(String[] args) {
    MonFil travailleur = new MonFil();
    travailleur.start();
  }
  public void run() {
    // Place ton code ici.
  }
}

J'utilise la première méthode dans la classe ExempleAvecFils parce que cette classe hérite déjà de JFrame et qu'on ne peut pas hériter de plus d'une classe en Java.

 
Sélectionnez
import javax.swing.*;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class ExempleAvecFils extends JFrame 
                       implements ActionListener, Runnable {
    
  // Constructeur
  ExempleAvecFils() {
    // Crée un cadre contenant un bouton et un champ textuel.
    GridLayout disposition = new GridLayout(2,1); 
    this.getContentPane().setLayout(disposition);
    JButton monBouton = new JButton("Tuer le temps");
    monBouton.addActionListener(this);
    this.getContentPane().add(monBouton);    
    this.getContentPane().add(new JTextField());
  }

  public void actionPerformed(ActionEvent événement) {
    // Crée un fil et exécute le code "tuer le temps"
    // sans bloquer la fenêtre.
    Thread travailleur = new Thread(this);
    travailleur.start();  // Ceci appelle la méthode run()
  }

  public void run() {
    // Tue juste un peu le temps pour montrer que
    // les contrôles de la fenêtre NE sont PAS verrouillés.
    for (int i = 0; i < 30000; i++) {
      this.setTitle("i = " + i);    
    }
  }
    
  public static void main(String[] args) {
    ExempleAvecFils maFenêtre = new ExempleAvecFils();
    // Permet la fermeture de la fenêtre par clic sur la 
    // petite croix dans le coin.
    maFenêtre.setDefaultCloseOperation(
                             WindowConstants.EXIT_ON_CLOSE);
        
    // Affecte sa taille au cadre et le rend visible.
    maFenêtre.setBounds(0, 0, 200, 100);
    maFenêtre.setVisible(true);
  }
}

La classe ExempleAvecFils démarre un nouveau fil d'exécution lorsque tu cliques sur le bouton Tuer le temps. Après quoi, le fil d'exécution contenant la boucle et le fil d'exécution principal utilisent chacun leur tour des tranches du temps du processeur. Tu peux maintenant saisir du texte dans le champ textuel (le fil d'exécution principal) alors que l'autre fil d'exécution exécute la boucle !

Les fils d'exécution méritent une étude bien plus approfondie que ces quelques pages et je t'encourage à en améliorer ta compréhension par d'autres lectures.

14-D. Fin du jeu de ping-pong

Après cette rapide introduction des fils d'exécution, nous sommes en mesure de modifier le code des classes de notre jeu de ping-pong.

Commençons par la classe TableVertePingPong. Nous n'avons pas besoin d'afficher un point blanc quand l'utilisateur clique - il s'agissait juste d'un exercice pour apprendre comment afficher les coordonnées du pointeur de la souris. Nous allons donc retirer la déclaration de la variable point et les lignes qui dessinent le point blanc de la méthode paintComponent(). Le constructeur n'a plus besoin non plus d'ajouter MouseListener, qui ne sert qu'à afficher les coordonnées du point.

Par contre, cette classe devrait traiter certaines touches du clavier (N pour nouvelle partie, S pour servir et Q pour quitter le jeu). La méthode addKeyListener() s'en charge.

Pour que notre code soit un peu plus encapsulé, j'ai aussi déplacé les appels à la méthode repaint() de la classe moteur à TableVertePingPong. Celle-ci a maintenant la responsabilité de se repeindre lorsque c'est nécessaire.

J'ai aussi ajouté deux méthodes, pour modifier les positions de la balle et de la raquette de l'ordinateur et pour afficher des messages.

 
Sélectionnez
package écrans;

import javax.swing.JPanel;
import javax.swing.JFrame;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.WindowConstants;
import java.awt.Dimension;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Color;
import moteur.MoteurJeuPingPong;
/**
*  Cette classe dessine la table de ping-pong, la balle et 
*  les raquettes et affiche le score
*/
public class TableVertePingPong extends JPanel 
                           implements ConstantesDuJeu {    
  private JLabel label;
    
  private int raquetteOrdinateur_Y = 
                            RAQUETTE_ORDINATEUR_Y_DEPART;
  private int raquetteEnfant_Y = RAQUETTE_ENFANT_Y_DEPART;
  private int balle_X = BALLE_X_DEPART;
  private int balle_Y = BALLE_Y_DEPART; 
    
  Dimension taillePréférée = new 
                     Dimension(LARGEUR_TABLE, HAUTEUR_TABLE);
    
  // Cette méthode affecte sa taille au cadre.
  // Elle est appelée par Java.
  public Dimension getPreferredSize() {
    return taillePréférée;
  }
    
  // Constructeur. Crée un récepteur d'événements souris. 
  TableVertePingPong() {
 
    MoteurJeuPingPong moteurJeu = 
                                 new MoteurJeuPingPong(this);
    // Reçoit les mouvements de la souris pour déplacer la 
    // raquette.
    addMouseMotionListener(moteurJeu);
    // Reçoit les événements clavier. 
    addKeyListener(moteurJeu);
  }
    // Ajoute à un cadre la table et un JLabel 
  void ajouteAuCadre(Container conteneur) {
    conteneur.setLayout(new BoxLayout(conteneur,
                                      BoxLayout.Y_AXIS));
    conteneur.add(this);
    label = new JLabel(
      "Taper N pour une nouvelle partie, S pour servir" +
      " ou Q pour quitter");
    conteneur.add(label);
  }

  // Repeint la fenêtre. Cette méthode est appelée par Java 
  // quand il est nécessaire de rafraîchir l'écran ou quand 
  // la méthode repaint() est appelée.
  public void paintComponent(Graphics contexteGraphique) {
        
    super.paintComponent(contexteGraphique);  
    // Dessine la table verte
    contexteGraphique.setColor(Color.GREEN);
    contexteGraphique.fillRect(
                  0, 0, LARGEUR_TABLE, HAUTEUR_TABLE); 
        
    // Dessine la raquette droite
    contexteGraphique.setColor(Color.yellow);
    contexteGraphique.fillRect(
                  RAQUETTE_ENFANT_X,  
                  raquetteEnfant_Y,
                  LARGEUR_RAQUETTE, LONGUEUR_RAQUETTE); 

    // Dessine la raquette gauche
    contexteGraphique.setColor(Color.blue);
    contexteGraphique.fillRect(RAQUETTE_ORDINATEUR_X, 
                       raquetteOrdinateur_Y, 
                       LARGEUR_RAQUETTE, LONGUEUR_RAQUETTE); 

    // Dessine la balle
    contexteGraphique.setColor(Color.red);
    contexteGraphique.fillOval(balle_X, balle_Y, 10, 10);        
    // Dessine les lignes blanches
    contexteGraphique.setColor(Color.white);
    contexteGraphique.drawRect(10, 10, 300, 200);
    contexteGraphique.drawLine(160, 10, 160, 210);

    // Donne le focus à la table, afin que le récepteur de 
    // touches envoie les commandes à la table
    requestFocus();
 }
    
 // Affecte sa position courante à la raquette de l'enfant
  public void positionnerRaquetteEnfant_Y(int y) {
    this.raquetteEnfant_Y = y;
    repaint();
  }
  
  // Retourne la position courante de la raquette de l'enfant
  public int coordonnéeRaquetteEnfant_Y() {
     return raquetteEnfant_Y;
  }

  // Affecte sa position courante à la raquette de 
  // l'ordinateur
  public void positionnerRaquetteOrdinateur_Y(int y) {
    this.raquetteOrdinateur_Y = y;
    repaint();
  }
    
  // Affecte le texte du message du jeu
  public void affecterTexteMessage(String texte) {
    label.setText(texte);
    repaint();
  }
    
  // Positionne la balle
  public void positionnerBalle(int x, int y) {
    balle_X = x;
    balle_Y = y;
    repaint();
  }

  public static void main(String[] args) {
 
    // Crée une instance du cadre
    JFrame monCadre = new JFrame("Table verte de ping-pong");

    // Permet la fermeture de la fenêtre par clic sur la 
    // petite croix dans le coin.
    monCadre.setDefaultCloseOperation(
                              WindowConstants.EXIT_ON_CLOSE);
    TableVertePingPong table = new TableVertePingPong();
    table.ajouteAuCadre(monCadre.getContentPane());
 
    // Affecte sa taille au cadre et le rend visible.
    monCadre.setBounds(0, 0, LARGEUR_TABLE + 5, 
                                         HAUTEUR_TABLE + 40);
    monCadre.setVisible(true);
  }
}

J'ai ajouté quelques variables déclarées final à l'interface ConstantesDuJeu. Tu devrais pouvoir en deviner le rôle d'après leurs noms.

 
Sélectionnez
package écrans;
/**
 * Cette interface contient toutes les définitions des 
 * variables invariantes utilisées dans le jeu. 
 */ 
public interface ConstantesDuJeu {
  // Taille de la table de ping-pong
  public final int LARGEUR_TABLE = 320; 
  public final int HAUTEUR_TABLE = 220;
  public final int HAUT_TABLE = 12;
  public final int BAS_TABLE = 180;

  // Incrément du mouvement de la balle en pixels 
  public final int INCREMENT_BALLE = 4;
    
  // Coordonnées maximum et minimum permises pour la balle  
  public final int BALLE_X_MIN = 1 + INCREMENT_BALLE;
  public final int BALLE_Y_MIN = 1 + INCREMENT_BALLE;
  public final int BALLE_X_MAX = 
                          LARGEUR_TABLE - INCREMENT_BALLE;
  public final int BALLE_Y_MAX = 
                          HAUTEUR_TABLE - INCREMENT_BALLE;
    
  // Position de départ de la balle
  public final int BALLE_X_DEPART = LARGEUR_TABLE / 2;
  public final int BALLE_Y_DEPART = HAUTEUR_TABLE / 2;
     
  // Taille, positions et incrément des raquettes 
  public final int RAQUETTE_ENFANT_X = 300;
  public final int RAQUETTE_ENFANT_Y_DEPART = 100;
  public final int RAQUETTE_ORDINATEUR_X = 15;
  public final int RAQUETTE_ORDINATEUR_Y_DEPART = 100;
  public final int INCREMENT_RAQUETTE = 2;
  public final int LONGUEUR_RAQUETTE = 30;
  public final int LARGEUR_RAQUETTE = 5;
    
  public final int SCORE_GAGNANT = 21;
  
  // Ralentit mes ordinateurs rapides ;
  // modifier la valeur si nécessaire.
  public final int DUREE_SOMMEIL = 10; // En millisecondes.
    
}

Voici les modifications les plus marquantes que j'ai effectuées dans la classe MoteurJeuPingPong :

  • J'ai supprimé l'interface MouseListener et toutes ses méthodes, puisque nous ne nous intéressons plus aux clics. MouseMotionListener se charge de tous les déplacements de la souris.
  • Cette classe implante maintenant l'interface Runnable ; les prises de décisions sont codées dans la méthode run(). Regarde le constructeur : j'y crée et lance un nouveau fil d'exécution. La méthode run() applique les règles de la stratégie du jeu en plusieurs étapes, programmées à l'intérieur de la clause if (balleServie). C'est une version courte de if (balleServie == true).
  • Je te prie de noter l'utilisation de la clause conditionnelle if pour affecter une valeur à la variable rebondPossible à l'étape 1. En fonction de l'expression en surbrillance, cette variable prendra la valeur true ou la valeur false.
  • La classe implante l'interface KeyListener ; la méthode keyPressed() examine la lettre entrée au clavier pour démarrer la partie, quitter le jeu ou servir la balle. Le code de cette méthode permet à l'utilisateur de taper aussi bien des majuscules que des minuscules, par exemple N et n.
  • J'ai ajouté plusieurs méthodes private telles que afficherScore(), serviceEnfant() et balleSurLaTable(). Ces méthodes sont déclarées privées parce qu'elles ne sont utilisées que dans cette classe et que les autres classes n'ont même pas à connaître leur existence. C'est un exemple d'encapsulation.
  • Certains ordinateurs sont trop rapides, ce qui rend les déplacements de la balle difficiles à contrôler. C'est pourquoi j'ai ralenti le jeu en appelant la méthode Thread.sleep(). La méthode statique sleep() met ce fil d'exécution en pause pour le nombre de millisecondes qui lui est passé en argument.
  • Pour ajouter un peu de piment au jeu, la balle se déplace en diagonale quand la raquette de l'enfant la frappe. C'est pourquoi le code modifie non seulement la coordonnée X, mais aussi la coordonnée Y de la balle.
 
Sélectionnez
package moteur;

import java.awt.event.MouseMotionListener;
import java.awt.event.MouseEvent;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import écrans.*;
/**
 * Cette classe est un récepteur de souris et de clavier. 
 * Elle calcule les déplacements de la balle et des raquettes  
 * et change leurs coordonnées.
 */
public class MoteurJeuPingPong implements Runnable,            
       MouseMotionListener, KeyListener, ConstantesDuJeu {
        
  private TableVertePingPong table; // Référence à la table.
  private int raquetteEnfant_Y = RAQUETTE_ENFANT_Y_DEPART;
  private int raquetteOrdinateur_Y = 
                             RAQUETTE_ORDINATEUR_Y_DEPART;
  private int scoreEnfant; 
  private int scoreOrdinateur;
  private int balle_X; // position X de la balle
  private int balle_Y; // position Y de la balle
  private boolean déplacementGauche = true;
  private boolean balleServie = false;
   
  //Valeur en pixels du déplacement vertical de la balle.     
  private int déplacementVertical;  

  // Constructeur. Stocke une référence à la table.
  public MoteurJeuPingPong(TableVertePingPong tableVerte) {
    table = tableVerte;
    Thread travailleur = new Thread(this);
    travailleur.start();
  }
  // Méthodes requises par l'interface MouseMotionListener 
  // (certaines sont vides, mais doivent être incluses dans 
  // la classe de toute façon).

   public void mouseDragged(MouseEvent événement) {
   }
   
   public void mouseMoved(MouseEvent événement) {

    int souris_Y = événement.getY();

    // Si la souris est au-dessus de la raquette de l'enfant 
    // et que la raquette n'a pas dépassé le haut de la 
    // table, la déplace vers le haut ; 
    // sinon, la déplace vers le bas.
    if (souris_Y < raquetteEnfant_Y && 
            raquetteEnfant_Y > HAUT_TABLE) {
      raquetteEnfant_Y -= INCREMENT_RAQUETTE;
    } else if (raquetteEnfant_Y < BAS_TABLE) {
      raquetteEnfant_Y += INCREMENT_RAQUETTE;
    }
        
    // Affecte la nouvelle position de la raquette 
    table.positionnerRaquetteEnfant_Y(raquetteEnfant_Y);
  }
    
  // Méthodes requises par l'interface KeyListener.
  public void keyPressed(KeyEvent événement) {
    char touche = événement.getKeyChar();
    if ('n' == touche || 'N' == touche) { 
       démarrerNouvellePartie();
    } else if ('q' == touche || 'Q' == touche) {
    terminerJeu();
    } else if ('s' == touche || 'S' == touche) {
      serviceEnfant();
    }
  }

  public void keyReleased(KeyEvent événement) {}
 
  public void keyTyped(KeyEvent événement) {}
 
  // Démarre une nouvelle partie.
  public void démarrerNouvellePartie() {
    scoreOrdinateur = 0;
    scoreEnfant = 0;
    table.affecterTexteMessage("Scores - Ordinateur : 0"  
                             + "Enfant : 0");
    serviceEnfant();
  }

  // Termine le jeu.
  public void terminerJeu(){
    System.exit(0);
  }
  
  // La méthode run() est requise par l'interface Runnable. 
 
 public void run() {
     
    boolean rebondPossible = false; 
    while (true) {
    
      if (balleServie) { // Si la balle est en mouvement
        // Étape 1. La balle se déplace-t-elle vers la 
        // gauche ?
        if (déplacementGauche && balle_X > BALLE_X_MIN) {
          rebondPossible = (balle_Y >= raquetteOrdinateur_Y
            && balle_Y < (raquetteOrdinateur_Y +    
            LONGUEUR_RAQUETTE) ? true : false);
          balle_X -= INCREMENT_BALLE;

          // Ajoute un déplacement vertical à chaque 
          // mouvement horizontal de la balle. 
          balle_Y -= déplacementVertical; 

          table.positionnerBalle(balle_X, balle_Y);
        // La balle peut-elle rebondir ?
          if (balle_X <= RAQUETTE_ORDINATEUR_X  
              && rebondPossible) {
            déplacementGauche = false;           
          }
        }

        // Étape 2. La balle se déplace-t-elle vers la 
        // droite ?
        if (!déplacementGauche && balle_X <= BALLE_X_MAX) {
          rebondPossible = (balle_Y >= raquetteEnfant_Y &&
            balle_Y < (raquetteEnfant_Y +                      
            LONGUEUR_RAQUETTE) ? true : false);
         
          balle_X += INCREMENT_BALLE;
          table.positionnerBalle(balle_X, balle_Y);
          // La balle peut-elle rebondir ?    
          if (balle_X >= RAQUETTE_ENFANT_X && 
                                            rebondPossible) {
          déplacementGauche = true;
      }
      }
        
      // Étape 3. Déplace la raquette de l'ordinateur vers le
      //          haut ou vers le bas pour bloquer la balle.
      
      if (raquetteOrdinateur_Y < balle_Y 
                  && raquetteOrdinateur_Y < BAS_TABLE) {
        raquetteOrdinateur_Y += INCREMENT_RAQUETTE;
        } else if (raquetteOrdinateur_Y > HAUT_TABLE) {
        raquetteOrdinateur_Y -= INCREMENT_RAQUETTE;
        }
        table.positionnerRaquetteOrdinateur_Y(
                                      raquetteOrdinateur_Y);

        // Étape 4. Sommeiller un peu     
        try {
          Thread.sleep(DUREE_SOMMEIL);
        } catch (InterruptedException exception) {
        exception.printStackTrace();
        }
    
        // Étape 5. Mettre le score à jour si la balle est 
        // dans la surface verte, mais ne bouge plus.
        if (balleSurLaTable()) {
          if (balle_X > BALLE_X_MAX ) {
            scoreOrdinateur++;
            afficherScore();
          } else if (balle_X < BALLE_X_MIN) {
            scoreEnfant++;
            afficherScore();
          }
        }     
      } // Fin du if balleServie    
    } // Fin du while
  }// Fin de run()

  // Sert depuis la position courante de la raquette 
  // de l'enfant.
  private void serviceEnfant() {

    balleServie = true;
    balle_X = RAQUETTE_ENFANT_X - 1;
    balle_Y = raquetteEnfant_Y;

    if (balle_Y > HAUTEUR_TABLE / 2) {
    déplacementVertical = -1;
    } else {
    déplacementVertical = 1;
    }
    
    table.positionnerBalle(balle_X, balle_Y);
    table.positionnerRaquetteEnfant_Y(raquetteEnfant_Y);
  }
  
  private void afficherScore() {
    balleServie = false;
     
    if (scoreOrdinateur == SCORE_GAGNANT) {
      table.affecterTexteMessage("L'ordinateur a gagné ! " +
                                scoreOrdinateur +  
                                " : " +  scoreEnfant);
    } else if (scoreEnfant == SCORE_GAGNANT) {
      table.affecterTexteMessage ("Tu as gagné ! "+ 
                                  scoreEnfant +
                                  " : " + scoreOrdinateur);
    } else {
      table.affecterTexteMessage ("Ordinateur : "+ 
                                 scoreOrdinateur +
                                 " Enfant: " + scoreEnfant);
    }
  }
 
  // Vérifie que la balle n'a pas dépassé la limite  
  // inférieure ou supérieure de la table.
  private boolean balleSurLaTable() {
    if (balle_Y >= BALLE_Y_MIN && balle_Y <= BALLE_Y_MAX) {
      return true;
    } else {
      return false;
    }
  }
}

Félicitations ! Tu as terminé ton second jeu. Compile les classes et fais une partie. Lorsque tu te sentiras à l'aise avec le code, essaie de le modifier ; je suis sûr que tu as des idées d'amélioration.

14-E. Que lire d'autre sur la programmation de jeux ?

1. CodeRally est un jeu de programmation temps réel Java parrainé par IBM, basé sur la plate-forme Eclipse. Il permet aux utilisateurs peu familiers de Java de rentrer facilement dans la compétition tout en apprenant le langage Java. Les joueurs développent une voiture de course et prennent des décisions sur le bon moment pour accélérer, tourner ou ralentir en fonction de la position des autres joueurs ou des points de contrôle, de leur niveau de carburant et d'autres facteurs.

http://www.alphaworks.ibm.com/tech/codeRally

2. Robocode est un jeu de programmation très amusant qui t'apprend le Java en te faisant créer des robots.

http://www.alphaworks.ibm.com/tech/robocode

14-F. Autres lectures

Image non disponible

Didacticiel sur les fils d'exécution Java :
http://java.sun.com/ (…) /threads/
Introduction aux fils d'exécution Java :
http://www-106.ibm.com/ (…) /j-dw-javathread-i.html
Classe java.awt.Graphics :
http://java.sun.com/ (…) /Graphics.html

14-G. Exercices

Image non disponible

1. La classe MoteurJeuPingPong affecte ses coordonnées au point blanc à l'aide de ce code :
table.point.x = événement.getX();.
Dans la classe TableVertePingPong, rend privée la variable point et ajoute la méthode publique :
affecteCoordonnéesPoint(int x, int y).
Modifie le code de la classe moteur en utilisant cette méthode.
2. Notre jeu de ping-pong a un bogue : après qu'un gagnant ait été annoncé, on peut toujours appuyer sur la touche S du clavier et le jeu continue. Corrige ce bogue.

14-H. Exercices pour les petits malins

Image non disponible

1. Essaie de modifier les valeurs de INCREMENT_RAQUETTE et de INCREMENT_BALLE. Des valeurs plus élevées augmentent les vitesses de déplacement de la raquette et de la balle. Modifie le code pour permettre à l'utilisateur de sélectionner un niveau de 1 à 10. Utilise la valeur sélectionnée comme incrément pour la balle et la raquette.
2. Quand la raquette de l'enfant frappe la balle dans la partie supérieure de la table, la balle se déplace en diagonale vers le haut et sort rapidement de la table. Modifie le programme pour que la balle se déplace en diagonale vers le bas quand elle est dans la partie supérieure de la table et vers le haut lorsqu'elle est dans la partie inférieure.


précédentsommairesuivant

Copyright © 2015 Yakov Fain. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.