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 :
- La partie se poursuit jusqu'à ce que l'un des joueurs (l'enfant ou l'ordinateur) atteigne le score de 21.
- Les mouvements de la raquette de l'enfant sont contrôlés par la souris.
- Le score est affiché en bas de la fenêtre.
- Une nouvelle partie commence quand un joueur appuie sur la touche N du clavier. Q met fin à la partie. S effectue le service.
- Seul l'enfant peut servir.
- Pour gagner un point, la balle doit dépasser la ligne à la verticale de la raquette (sans avoir été bloquée par la raquette).
- Quand l'ordinateur renvoie la balle, elle ne peut se déplacer qu'horizontalement vers la droite.
- 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 :
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 :
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.
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.
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 :
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 :
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.
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.
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.
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.
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 :
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 :
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().
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.
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.
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.
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.
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.
14-F. Autres lectures▲
Didacticiel sur les fils d'exécution Java : |
14-G. Exercices▲
1. La classe MoteurJeuPingPong affecte ses coordonnées au point blanc à l'aide de ce code : |
14-H. Exercices pour les petits malins▲
|
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. |