Portail informatique Année 2018 – 2019

CSC 3101 – Algorithmique et langage de programmation

L'objectif de cet exercice est de vous faire manipuler des classes anonymes et de vous introduire la bibliothèque JavaFX qui permet de créer facilement des applications graphiques.

Durée des exercices obligatoires : 2h30mn en présentiel
  • Exercice 1 : obligatoire (facile, ∼10mn)
  • Exercice 1 : obligatoire (facile, ∼45mn)
  • Exercice 2 : obligatoire (facile, ∼30mn)
  • Exercice 3 : obligatoire (moyen, ∼30mn)
  • Exercice 4 : obligatoire (moyen, ∼45mn)
Dans cet exercice, nous concevons un jeu vidéo simple nommé Alien vs Pinapples : une armée d'ananas mutants attaque votre galaxie et vous jouez un alien en charge de sauver l'univers, yipa ! Cet exercice n'est à faire que si vous utilisez les machines qui vous sont fournies en salle TP.

L'installation actuelle de Java sur les machines des salles de TP ne permet pas faire des programmes JavaFX facilement. Nous sommes actuellement en train de voir comment faciliter le démarrage avec la DISI, mais cet exercice a pour but de vous permettre de quand même effectuer le TP. Effectuez donc très sérieusement chaque étape décrite dans cet exercice si vous utilisez les machines fournie par Telecom SudParis ! Commencez par effectuer la procédure de création de projet. Avant de saisir le nom du projet, il faut cliquer sur le lien Configure JREs qui se trouve vers le milieu de la fenêtre.

Dans la fenêtre qui vient de s'ouvrir, cliquez sur le bouton Add. Sélectionnez Standard JVM, puis cliquez sur Next. Dans la zone de saisie JRE Home, copiez-collez le texte suivant : /mci/inf/thomas_g/jfx/jdk1.8.0_131. Vous devriez voir tout un tas de lignes s'afficher dans la zone JRE System libraries. Cliquez alors sur Finish. Dans la fenêtre dans laquelle vous venez de revenir indiquant les Installed JREs, Sélectionnez jdk1.8.0_131 et cliquez sur ok.

Enfin, dans la fenêtre de création de projets, sélectionnez Use default JRE (currently jdk1.8.0_131) puis saisissez le nom de votre projet (Project Name) : alien avant de cliquer sur Finish.
Dans ce premier exercice, nous construisons une fenêtre avec JavaFX dans laquelle nous affichons une image. Cet exercice a pour but de vous présenter les concepts de base de la bibliothèque JavaFX.

La bibliothèque JavaFX gère des fenêtres représentées par des instances de la classe javafx.stage.Stage. Un Stage peut ensuite afficher une scène, c'est-à-dire un ensemble d'éléments. Une scène est représentée par la classe javafx.scene.Scene. Le développeur peut définir plusieurs scènes pour la même fenêtre, ce qui lui permet de passer d'une scène à l'autre facilement. Dans notre cas, une unique scène sera largement suffisant.

Une scène est constituée d'un ensemble d'éléments. Techniquement, une scène affiche un arbre d'éléments, chaque élément héritant de javafx.scene.Node. Les feuilles de l'arbre sont directement des objets affichés : des formes comme des cercles ou des polygones, des boutons, ou encore des objets plus complexes comme le canevas que l'ont va étudier dans cet exercice, Les nœuds intermédiaires sont des instances de la classe javafx.scene.Group. Une instance de la classe Group est un conteneur à éléments gérant la disposition interne des éléments qu'il contient.

La figure ci-dessous illustre l'architecture d'une application JavaFX. Une fenêtre est représentée par une instance de la classe Stage. Celle-ci affiche une instance de la classe Scene. La scène est associée à un nœud, c'est-à-dire une instance de la classe Node. Dans la figure, ce nœud est un Group et il contient un cercle, un bouton et un sous-groupe. --------- --------- --------- | Stage | ----> | Scene | ----> | Group | --------- --------- --------- / | \ / | \ / | \ / | \ ---------- ---------- ---------- | Cercle | | Button | | Group | ---------- ---------- ---------- / | \ ............. Pour initialiser JavaFX, il faut créer une classe qui hérite de javafx.application.Application. Dans notre exercice, nous créons donc la classe nommée tsp.alien.Alien héritant de javafx.application.Application.

La classe Application offre une méthode publique statique nommée void launch(String... args). Cette méthode permet de démarrer une application avec une unique fenêtre. Elle alloue une instance de la classe qui a généré l'appel (dans notre cas, une instance de tsp.alien.Alien), prépare le moteur graphique, crée une fenêtre (une instance de Stage) et finalement délègue le remplissage de la fenêtre à la méthode d'instance void start(Stage primaryStage). C'est en redefinissant cette méthode dans tsp.alien.Alien que nous pouvons associer une scène à la fenêtre et des éléments à la scène avant d'afficher la fenêtre avec la méthode void Stage.show().

À cette étape, nous nous préoccupons uniquement de rendre une fenêtre visible sans essayer d'y associer une scène et des éléments. Pour cela, dans le projet alien, créez une classe nommée Alien appartenant au package tsp.alien. Ensuite, copiez-collez le code qui suit dans le fichier Alien. Lorsque vous lancez votre programme, vous devriez voir apparaître une fenêtre sans aucune décoration ni titre. Pensez à fermer cette fenêtre pour quitter votre programme. Il est possible que Eclipse vous indique de nombreuses erreurs cette étape. Si c'est le cas, cliquez sur besoin d'aide.

Par défaut, Eclipse considère qu'il vaut mieux ne pas utiliser JavaFX car cette bibliothèque n'a été que récemment intégrée à la bibliothèque standard Java. Pour corriger ce problème, il faut cliquer dans le menu Project puis dans le sous-menu Properties.

Dans la fenêtre ouverte par Eclipse, il faut ensuite sélectionner Java Build Path dans la partie gauche de la fenêtre. Dans la fenêtre qui apparaît, il faut ensuite sélectionner l'onglet Libraries (partie droite de la fenêtre). Vous devriez pouvoir dérouler un menu JRE System Library ce qui devrait vous donner une fenêtre similaire à la fenêtre de gauche présentée dans la figure ci-dessous.

En Cliquant sur la ligne Access rule, vous devrez voir une fenêtre similaire à celle du milieu sur la figure ci-dessous, mais sans la règle javafx. Cliquez sur add, vous devriez voir une fenêtre similaire à celle de droite. Ajoutez une règle Accessible (par défaut la règle est à Forbidden). Autorisez ensuite tous les packages de JavaFX en mettant javafx/** dans Rule Pattern. Vous devriez obtenir l'affichage représenté sur la figure de droite. Après avoir cliqué sur OK, vous devriez voir votre règle autorisant JavaFX, comme dans la fenêtre représentée sur la figure du milieu.

Si après ces manipulations vous avez encore des erreurs, n'hésitez pas à demander de l'aide à vos enseignants !
Nous pouvons maintenant ajouter un titre à la fenêtre : Alien vs Pinapples. Pour cela, utilisez la méthode javafx.stage.Stage.setTitle. Le stage.show() doit, pendant tout l'exercice, rester la dernière opération exécutée dans la méthode start(), sinon, vous risquez de ne pas voir les décorations que vous ajoutez à votre fenêtre. Avant d'afficher des éléments intéressants dans notre fenêtre, il faut créer un groupe, une scène et les associer à la fenêtre. Pour cela, dans la méthode Alien.start, vous devez :

À cette étape, votre fenêtre ne devrait toujours rien contenir puisqu'on n'a pas encore ajouter d'élément au groupe.
Maintenant que nous avons un groupe, nous pouvons définir la zone dans laquelle nous affichons le jeu. Pour cela, nous utilisons la classe javafx.scene.canvas.Canvas. Un canevas permet d'afficher un dessin que vous fabriquerez dans les questions suivantes.

À cette question, nous nous occupons de fixer la taille du canevas à sa création : 694 pixels de largeur et 520 pixels de hauteur. De façon à pouvoir modifier facilement ces valeurs, définissez, dans la méthode Alien.start, deux variables stockant ces nombres et utilisez les dans la suite de l'exercice.

Ensuite, créez un canevas avec le constructeur Canvas(double width, double height). De façon à ajouter le canevas aux enfants du groupe associé à la scène, vous devez :

Enfin, comme nous ne nous soucions pas du redimensionnement de la fenêtre dans cet exercice, utilisez Stage.setResizable pour empêcher l'utilisateur de redimensionner la fenêtre. Vous pouvez vérifier que votre code est correct en vérifiant que (i) vous ne pouvez pas redimensionner votre fenêtre, et (ii) que la taille de la fenêtre a changé depuis la dernière question.
Notre canevas est maintenant prêt et nous pouvons commencer à dessiner son contenu. Pour dessiner le contenu d'un canevas, il faut en extraire un javafx.scene.canvas.GraphicsContext à l'aide la méthode javafx.scene.canvas.Canvas.getGraphicsContext2D.

À cette question, nous affichons le score du joueur. Comme pour le moment, le joueur n'a pas de score, nous affichons donc juste Score: 42. Après avoir extrait le javafx.scene.canvas.GraphicsContext du canevas, utilisez la méthode javafx.scene.canvas.GraphicsContext.fillText pour afficher le score à la position (540, 36).
Cet affichage est un peu neutre et ne correspond pas exactement à la philosophie d'un jeu comme Alien vs Pinapples. Nous décorons donc ce texte. Pour commencer, nous entourons le texte en utilisant javafx.scene.canvas.GraphicsContext.strokeText à la position (540, 36). Le rendu ne correspondant pas encore à un jeu d'aliens, nous changeons la police et les couleurs. Copiez coller le code suivant dans votre programme : gc.setFont(Font.font("Helvetica", FontWeight.BOLD, 24)); gc.setFill(Color.BISQUE); gc.setStroke(Color.RED); gc.setLineWidth(1); puis remplacez gc par le nom de variable qui référence le GraphicsContext.

Pensez à changer la police et les couleurs avant d'appeler fillText, sinon, fillText utilisera les polices et couleurs originale.
Nous pouvons maintenant mettre un fond pour créer l'ambiance du jeu. Pour cela, nous devons charger une image que nous devons ajouter à notre projet. Dans Eclipse, cliquez sur le Menu File, puis sur le sous-menu New et enfin sur le sous-menu Source Folder. Créez ensuite un répertoire que vous pouvez nommer resources. Le contenu de ce répertoire est directement ajouté aux classes Java de votre application, ce qui permet d'y accéder pendant l'exécution.

Ne vous trompez pas, utilisez bien le sous-menu Source Folder et non Folder

Ensuite, téléchargez l'image suivante (fournie par la Nasa) : space.jpg. Enfin, placez cette image dans le dossier resources. Pour cela, il faut cliquer à droite sur le dossier resources, sélectionner le sous-menu Import, double-cliquer sur File System et enfin retrouver où vous avez téléchargé l'image.
Maintenant que l'image est incluse dans votre projet, vous pouvez l'afficher dans votre canevas. Pensez à l'afficher avant d'afficher le score, car mettre le fond du canevas écrase l'ancien contenu du canevas. Pour afficher le fond, vous devez :
  • Utilisez le constructeur javafx.scene.image.Image(String, double, double, boolean, boolean) pour charger l'image. Le premier argument est le chemin vers l'image, c'est-à-dire space.jpg dans notre cas. Les deux arguments suivants sont la taille que nous souhaitons donner à notre image, c'est à dire la taille de la fenêtre dans notre cas. Les deux argument suivants doivent être positionnés à false : il ne faut pas préserver les ratios de l'image d'origine et il n'est pas nécessaire d'utiliser un algorithme de qualité pour mettre l'image à l'échelle.
  • Utilisez javafx.scene.canvas.GraphicsContext.drawImage pour afficher l'image aux coordonnées (0, 0) dans le canevas.
L'univers étant maintenant prêt, nous pouvons ajouter des lutins (sprites en anglais). Un lutin est un terme utilisé pour parler d'un objet ou d'un personnage pouvant se déplacer dans la fenêtre du jeu. Nous aurons besoin de deux types de lutins : un lutin représentant l'alien et des lutins représentant les ananas. La seule différence entre les deux types de lutins n'étant que leur image, une unique classe suffit pour les représenter. Commencez par ajouter les images associées aux lutins dans le répertoire resources de votre projet : alien.png et pinapple.png. Pour l'instant, un lutin doit posséder une image de type javafx.scene.image.Image, une largeur, une hauteur et des coordonnées (de type double) x et y. Ajoutez une classe tsp.alien.Sprite à votre projet et ajoutez les champs appropriés. Ajoutez aussi à votre lutin un constructeur prenant en paramètre :

Vérifiez que vous chargez correctement l'alien (largeur 62 et hauteur 36) et l'ananas (largeur 19 et hauteur 36). Comme vous n'affichez pas encore les lutins, il est normal qu'il ne se passe rien de nouveau d'un point de vue graphisme.
Nous pouvons maintenant afficher un lutin. Pour cela, ajoutez une méthode render à la classe Sprite prenant un GraphicsContext en paramètre et permettant d'afficher le lutin. Vérifiez que vous pouvez afficher vos lutins.
Maintenant que nous pouvons afficher des lutins, nous nous occupons de les déplacer. Pour cela, il faut être capable de mettre à jour le contenu du canevas régulièrement. JavaFX offre différentes possibilités. Nous utilisons la classe javafx.animation.AnimationTimer qui permet d'invoquer régulièrement la méthode abstraite javafx.animation.AnimationTimer.handle redéfinie par héritage. Techniquement, JavaFX invoque cette méthode 60 fois par seconde, mais il peut y avoir des décalages lorsque la méthode mets plus de 1/60ième de seconde à s'exécuter. Dans la méthode tsp.alien.Alien.start, créez une instance d'une classe anonyme héritant de javafx.animation.AnimationTimer. Ensuite, appelez la méthode javafx.animation.AnimationTimer.start sur cette instance pour démarrer le minuteur. Dans votre classe anonyme, définissez la méthode handle et utilisez System.out.println pour afficher un message dans le terminal/ Enfin, vérifiez que votre programme affiche bien régulièrement le message. Au lieu d'afficher un message à chaque invocation de handle, nous redessinons notre canevas. Supprimez le code affichant l'alien et un ananas de start. Ensuite, déplacer les codes affichant le fond d'écran et le score dans la méthode handle de la classe anonyme, ce qui permet de les redessiner 60 fois par secondes. Enfin, après avoir affiché le fond d'écran, mais avant d'afficher le score, affichez un ananas à une position aléatoire se trouvant dans les bornes de la fenêtre en utilisant la méthode java.lang.Math.random. Vérifiez que votre ananas sautille continuellement dans l'écran. Nous nous occupons maintenant de déplacer correctement les ananas. Ajoutez des champs xSpeed et ySpeed à la classe tsp.alien.Sprite. Ces champs représentent le déplacement, en nombre de pixels, que doit faire le lutin à chaque appel à handle. Ajoutez aussi une méthode setSpeed(double xSpeed, double ySpeed) permettant de modifier la vitesse de déplacement d'une lutin. Finalement, ajoutez une méthode update() à un lutin, permettant de déplacer les positions du lutin de xSpeed suivant l'axe des x et de ySpeed suivant l'axe des y.

Après avoir supprimé le code permettant d'afficher un ananas à une position aléatoire, positionnez initialement un ananas à la position (100,100), donnez lui une vitesse de (1, 1), et mettez à jour sa position à chaque appel à handle avant de l'afficher. Vous devriez voir un ananas qui se déplace tranquillement jusqu'aux bords de la fenêtre avant de disparaître.
Courir après un ananas en dehors de la fenêtre n'est pas chose aisée pour un joueur. Lorsqu'un ananas arrive sur un bord, nous vous proposons de le faire rebondir. Si l'ananas atteint les limites de la fenêtre suivant l'axe des x, il faut inverser sa vitesse suivant cet axe. De façon similaire, si l'ananas atteint les limites de la fenêtre suivant l'axe des y, il faut inverser sa vitesse suivant cet axe. Ajoutez une méthode validatePosition() que vous appellerez à la fin de update et qui :
  • s'assure que l'ananas est toujours entièrement visible dans la fenêtre (attention, pensez que votre ananas à une largeur et une hauteur),
  • s'occupe de faire rebondir l'ananas lorsqu'il atteint un des 4 bords de la fenêtre.

Pour mettre en œuvre validatePosition, vous avez besoin d'accéder à la taille de la fenêtre à partir d'une instance de Sprite. Le plus simple est d'ajouter deux champs pour stocker cette taille dans la classe Sprite et de les initialiser via le constructeur. Si votre programme est correct, vous devriez voir un ananas qui rebondit gentiment sur les bords de l'écran.
Nous pouvons maintenant créer une armée d'ananas, un peu comme nous avions créé une armée de monstre (voir le CI3). Nous vous proposons d'utiliser le tableau extensible fourni par la bibliothèque Java pour stocker vos lutins ( java.util.ArrayList).

Au lieu de créer un unique ananas dans Alien.start(), créez 15 ananas que vous stockerez dans votre tableau extensible. Initialisez la position initiale de chaque ananas à une valeur aléatoire se trouvant dans les limites de la fenêtre, et le vecteur de vitesse de chaque ananas à une valeur comprise entre -5 et 5.

Dans AnimationTimer.handle, mettez à jour la position des ananas avant de les afficher. Comme nous aurons besoin de supprimer des lutins dans la suite de l'exercice, plutôt que d'utiliser une boucle sur une collection pour parcourir votre tableau extensible de lutins, nous vous demandons d'utiliser explicitement un itérateur (voir java.util.Collection.iterator et java.util.Iterator). Vous devriez maintenant voir un univers peuplé d'ananas rebondissants sur les bords de la fenêtre.
Maintenant que des ananas se promènent dans l'univers, il faut créer un alien pour les arrêter avant qu'ils ne fassent trop de dégâts. L'alien est créé à une position initiale (par exemple 100x100). Le joueur contrôle l'alien avec le clavier. Lorsqu'il presse une des flèches, la vitesse de l'alien est incrémentée de 1 suivant l'axe de la flèche. Il faut donc intercepter les événements claviers. Pour cela, il faut appeler la méthode javafx.scene.Scene.setOnKeyPressed en lui fournissant en argument une instance d'une classe anonyme héritant de javafx.event.EventHandler. Ce javafx.event.EventHandler doit être paramétré par le type javafx.scene.input.KeyEvent. Dans la classe anonyme, redéfinissez la méthode javafx.event.EventHandler.handle(KeyEvent e) de façon à ajuster la vitesse de déplacement de l'alien. Le code de la touche pressé est donné par la méthode javafx.scene.input.KeyEvent.getCode. Les flèches sont respectivement associées aux valeurs KeyCode.LEFT, KeyCode.RIGHT, KeyCode.UP et KeyCode.DOWN. Pensez à mettre à jour la position de l'alien avec la méthode update() et à l'afficher dans handle. Vérifiez que vous arrivez bien à déplacer l'alien quand vous pressez les flèches.

La classe KeyCode est ce qu'on appelle une énumération, ce qui signifie que les valeurs KeyCode.LEFT, KeyCode.RIGHT, KeyCode.UP et KeyCode.DOWN sont des constantes. Pour savoir quelle touche a été pressée, vous pouvez simplement utiliser un switch : switch(code) { case LEFT: ...; break; case RIGHT: ...; break; case UP: ...; break; case DOWN: ..; break; default: }
Lorsqu'un alien touche un ananas, le joueur gagne 100 points et l'ananas disparaît. Ajoutez un champ score initialisé à 0 dans la classe tsp.alien.Alien. Ensuite, modifiez le code de handle de façon à identifier les ananas touchés par l'alien. Pour cela, ajoutez une méthode d'instance boolean tsp.alien.Sprite.intersects(Sprite s) renvoyant vrai si les rectangles dans lesquels se trouvent les deux lutins se touchent. Dans handle, lorsque vous parcourez la liste des ananas, après avoir mis à jour la position d'un ananas, supprimez l'ananas du tableau d'ananas si il est touché par l'alien en utilisant votre iterateur, puis incrémentez de 100 le score du joueur. Pensez aussi à afficher le vrai score du joueur dans handle(). Pour quelle raison le programme ne compile plus si vous déplacez la variable score dans la méthode start, alors qu'il compile si vous la déplacez dans la classe anonyme héritant de javafx.animation.AnimationTimer ? Une classe anonyme est une classe interne de méthode sans nom :
  • Si score est un champ de classe anonyme, on peut bien sûr y accéder en lecture/écriture à partir de la méthode de la classe anonyme.
  • Si score est une variable de la méthode Alien.start, comme le classe anonyme est une classe interne de méthode, Java effectue une copie de score dans l'instance de la classe anonyme lorsque l'instance est créée. De façon à éviter que les scores de la méthode et de l'instance de la classe anonyme divergent, Java interdit tout accès en écriture au champ score à partir de la création de l'instance de la classe anonyme. Comme score est modifié dans la méthode handle de la classe anonyme (c'est-à-dire qu'on y accède en écriture dans la méthode handle), on ne peut pas définir score comme variable de la méthode start().
  • Si score est un champ de la classe Alien, la classe anonyme y accède via son champ Alien.this, et ce champ peut bien être accédé en lecture/écriture.
En tant que développeur, vous trouvez que de supprimer les ananas en déplaçant l'alien avec les flèches devient vite fastidieux pour faire des tests. On vous propose donc de créer un mode triche qui utilise la souris : lorsque l'utilisateur clique quelque part sur le canevas, le vaisseau doit se déplacer à la position cliquée et prendre une vitesse nulle. Pour cela, il faut créez une instance d'un classe anonyme héritant de javafx.event.EventHandler paramétrée par le type javafx.scene.input.MouseEvent. Les méthodes javafx.scene.input.MouseEvent.getX et javafx.scene.input.MouseEvent.getY permettent de connaître la position de la souris. Ensuite, il faut enregistrer ce gestionnaire d'événement via javafx.scene.Scene.setOnMousePressed qui est invoqué dès que l'utilisateur commence à cliquer sur le souris, et via javafx.scene.Scene.setOnMouseDragged qui indique le déplacement dans la souris pendant que l'utilisateur clique dessus. Félicitations, depuis le début du module, vous avez développé entre 1500 et 2000 lignes de code !

Vous savez maintenant programmer en Java !

Pour les étudiants curieux et qui veulent apprendre à utiliser de nouvelles bibliothèques, n'hésitez pas à aller consulter les incroyables tutoriels de Jean-Michel Doudoux qui regroupent à ce jour 117 chapitres répartis en 17 parties. Pensez aussi que la documentation officielle Java constitue une inestimable source d'information.