Préambule

L'objectif de ce premier cours est de voir comment créer les bases d'un jeu en javafx où un élément bouge en fonction des interactions de l'utilisateur. En l’occurrence, il s'agit d'une simple balle qui rebondit sur les murs délimitant la fenêtre de visualisation.

La conception des classes doit suivre le paradigme MVC, ce qui nécessite d'analyser correctement les besoins applicatifs et d'en déduire les attributs et méthodes de chaque classe. Cependant, compte tenu du fait que l'on utilise javafx, il y a une structure générale à suivre qui est commune à tout jeu et dont il faut tenir compte pour créer les classes des éléments du jeu. A noter que cette structure générale est celle qui est utilisée dans gamifier, mais de façon plus poussée afin de tenir compte des nombreuses caractéristiques possibles des jeux (niveaux, animations, ...).

 

Pour suivre correctement ce cours, il est conseillé d'ouvrir en parallèle les sources, dont l'archive est téléchargeable [ ici ] 

1°/ Les besoins

  • la taille de la fenêtre de l'application doit être paramétrable.
  • des rectangles opaques, représentant des murs, doivent être placés sur les bords de la fenêtre.
  • une balle, représentée par un disque d'une couleur à déterminer, avec au centre un plus petit disque d'une autre couleur, doit être placée dans la fenêtre au démarrage de l'application.

 

  • lorsque l'utilisateur appuie sur les flèches du clavier, la balle soit se déplacer dans la direction indiquée par la flèche.
  • il n'y a pas besoin de rester appuyé pour que la balle continue de se déplacer (= mouvement à inertie)
  • lorsque l'utilisateur appuie sur la touche H (comme hit), la direction de la balle (en degré) est tirée aléatoirement entre 0 et 360. Elle conserve la vitesse qu'elle avait.
  • lorsque l'utilisateur appuie sur la touche U (comme up) ou D (comme down), la vitesse de la balle est respectivement augmentée ou diminuée.
  • lorsque l'utilisateur appuie sur la touche espace, la balle s'arrête à sa position actuelle.
  • lorsque l'utilisateur appuie sur la touche backspace, et que la balle est déjà arrêtée, elle est déplacée à sa prochaine position compte tenu de sa vitesse (= mode debug).
  • lorsque l'utilisateur appuie sur la touche S, la balle reprend son mouvement continu avec sa direction et vitesse courante.

 

  • lorsque le balle entre en collision avec un mur, elle rebondit selon les lois physiques des chocs élastiques sans frottement, c.a.d comme un rayon lumineux sur une surface.
  • juste après le choc, la balle doit changer de couleur pendant environ 1/10s.

 

2°/ La structure générale.

Afin de suivre de bonnes pratiques de codage, et en supposant que l'application soit crée sous idea, le répertoire src des sources du projet contient :

  • Baballe.java : le fichier contenant main()
  • model : le répertoire/package contenant la partie modèle, à savoir :
    • Model.java : le modèle "général",
    • BallModel.java : pour définir le modèle de la balle.
  • view : le répertoire/package contenant la partie vue, à savoir :
    • View.java : la vue "générale".
    • BallLook.java : pour définir l'aspect visuel de la balle
  • control : le répertoire/package contenant la partie contrôle, à savoir :
    • Controller.java : le contrôleur "général"
    • ControllerAnimation.java : pour gérer le défilement des frames du jeu et donc la mise à jour régulière du modèle et de la vue.
    • ControllerKeyboard.java : pour gérer les interactions clavier.

 

3°/ Partie modèle

Cette partie doit être implémentée de façon indépendante du reste, à partir du moment où elle est bien conçue. On va donc commencer par coder ses classes.

3.1°/ Model.java

Cette classe contient les attributs nécessaires à la gestion générale du jeu. Il y a donc au moins :

  • ce qui permet de régler la durée des frames, et de compter le temps passé depuis la dernière frame.
  • les dimensions de l'espace dans lequel sont placés les éléments du jeu.
  • les modèles des éléments du jeu.

Remarque 1 : les dimensions de l'espaces pourraient être placée dans la partie vue. Cependant, comme c'est la partie modèle qui gère la position des éléments dans l'espace, il est indispensable de connaître sa dimension. Par exemple, si on veut positionner la balle au centre de l'espace au lancement du jeu, il faut bien connaître les dimensions de l'espace. 

Remarque 2 : il est possible de gérer les positions des éléments dans la partie vue, ce qui invalide la remarque précédente. Cependant, ce n'est pas forcément pratique lorsque l'on veut régler des interactions entre éléments, selon leur position, ce qui n'est normalement pas dépendant de leur aspect visuel (sauf collision) et doit donc être fait dans le modèle.

3.2°/ BallModel.java

Comme la balle est amenée à se déplacer selon un angle entre 0 et 360°, avec une certaine vitesse, il est logique de définir 2 attributs représentant ces données. La valeur de la vitesse sera en nombre de pixels par frame. Cependant, on déplace la balle en modifiant ses coordonnées x,y. Il est donc souhaitable de calculer la vitesse en X et Y, à partir de l'angle et de la vitesse spatiale, ce qui nécessite 2 attributs supplémentaires.

Par ailleurs, le paradigme MVC impose que la vue accède au modèle pour se mettre à jour. Il doit donc y avoir toutes les informations nécessaires pour cette opération, par rapport aux besoins applicatifs. Pour trouver les attributs nécessaires, il suffit de réfléchir en terme d'événement ayant changé l'état de l'élément. Dans le cas présent :

  • événement "la balle a bougé" -> attribut booléen pour indiquer qu'elle a bougé + attribut de position.
  • événement "collision" -> attribut booléen pour indiquer la collision + attribut pour compter les frames.
  • événement "fin choc" quand le compteur arrive à 0 (au bout de 1/10s) -> attribut booléen pour indiquer la fin du choc.

En procédant ainsi, la partie vue peut facilement tester les attributs booléens pour en déduire comment modifier l'aspect/position visuelle de l'élément.

Pour les méthodes, il suffit de réfléchir à l'identique :

  • événement "la balle bouge" -> méthode pour déterminer sa prochaine position (utile pour les collisions) + méthode pour changer la position compte tenu des vitesses en X et Y.
  • événement "appui flèches clavier" ou "appui H" : méthode pour déterminer les vitesse en X et Y à partir de l'angle et la vitesse spatiale.
  • événement "appui U/D" : méthode pour augmenter/diminuer la vitesse spatiale.
  • événement "collision" : méthodes pour déterminer le nouvel angle.
  • événement "nouvelle frame" : méthode pour mettre à jour l'état de la balle en appelant les méthode ci-dessus + si collision activer le compteur de "fin de choc".

 

4°/ Partie vue

En javafx, le visuel d'une application est créé à partie des classes :

  • Stage : représente une fenêtre.
  • Scene : représente la partie centrale de la fenêtre, donc sans les cadres.
  • Node : la super-classe de tous les éléments javafx que l'on peut inclure dans un objet Scene.

Si l'application ne repose que sur une seule fenêtre, il n'y a pas besoin de la créer explicitement. En effet, la classe principale contenant main() doit hériter de la classe Application, et se faisant, elle DOIT définir une méthode start(), qui prend en paramètre un objet Stage. C'est donc la machinerie javafx qui crée la fenêtre initiale et la passe en paramètre à start() (cf. section 6)

IMPORTANT : normalement, rien n'impose que l'espace des modèles d'élément soit le même que celui du visuel des éléments. Cependant, il est beaucoup plus facile de déboguer et gérer l'ensemble des éléments quand il y a effectivement correspondance. Cela veut donc dire qu'un modèle d'élément positionné en 50,75 sera effectivement placé dans la fenêtre au pixel 50,75.

4.1°/ View.java

Cette classe permet de regrouper tous les éléments du visuel de l'application, excepté le Stage et la Scene. Dans le cas présent, on a donc :

  • le Node racine, sous la forme d'une instance de Pane. Cet élément aura pour fils tous les autres visuels composant la scène (NB : javafx impose cette structuration de la scène).
  • le visuel de la balle, donc une instance de BallLook.
  • les murs aux bords de la fenêtre, qui sont des instances de la classe Rectangle (sous classe de Shape).

Fonctionnellement, cette classe ne fait pas grand chose, à part créer les instances mentionnées ci-dessus et d'ajouter la balle et les murs comme fils du Pane racine.

 

Remarque : contrairement à la balle, il n'y a pas spécialement besoin de créer une classe de modèle pour les murs. En effet, ces derniers n'ont rien de spécial et ils sont uniquement utilisés dans la détection de collision. Or, celle-ci se base sur le visuel et non sur un modèle physique de collision.

 

4.2°/ BallLook.java

Pour représenter des éléments visuels dans la Scene, il existe principalement 2 moyens :

  • utiliser des images, ce qui implique de créer des instances de ImageView,
  • dessiner avec des formes géométriques, ce qui implique d'utiliser des instances des sous-classes de Shape, comme Circle, Rectangle, Line, ...

L'avantage de la seconde solution est que ça facilite la détection de collision : il existe une méthode permettant de trouver l'intersection entre 2 objets Shape. Si la taille de cette intersection est non nulle, il y a collision.

Cependant, le visuel d'un élément du jeu peut être constitué grâce à plusieurs Shape. Si les coordonnées du modèle de l'élément changent, il faut alors déplacer toutes les Shape. Pour gérer ce problème, la solution la plus simple consiste à mettre toutes ces Shape dans une instance de Group. Si on déplace le Group, tous ses fils sont déplacés de même.

Pour la détection de collision, il existe deux approches :

  • on teste les collisions avec la position courante des éléments,
  • on teste avec la prochaine position des éléments.

La deuxième solution évite que des éléments puissent se chevaucher à l'écran. C'est donc celle-ci qui est retenu pour ce cours. Le problème, c'est qu'il faut tester la collision sans pour autant déplacer réellement l'élément. Pour cela, on définit pour chaque visuel d'élément des Shape transparentes, qui décrivent l'enveloppe de l'élément. C'est cette enveloppe que l'on déplace à la prochaine position de l'élément et que l'on utilise pour les collisions. Cette enveloppe étant elle-même éventuellement constituée de plusieurs Shape, on utilise de nouveau un Group pour les stocker.

Fonctionnellement, cette classe doit essentiellement :

  • mettre à jour le visuel à chaque frame, en fonction des changements du modèle (cf. attributs booléens liés aux événements)
  • vérifier la collision avec une (ou plusieurs) Shape.

 

5°/ Partie contrôle

 

5.1°/ ControllerAnimation.java

Cette classe est quasi identique pour tous les jeux. Elle se contente de créer un timer dont la méthode handle() va être appelée régulièrement par la machinerie javafx, avec en paramètre le temps passé en µS depuis le lancement de l'application. On peut donc ainsi mesurer des intervalles de temps (par exemple 10ms), et à la fin de chaque intervalle, on demande au contrôleur principal de mettre à jour le modèle puis la vue.

5.2°/ ControllerKeyboard.java

Généralement, cette classe se contente de détecter les appuis sur les touches et d'appeler des méthodes du contrôleur général.

5.3°/ Controller.java

Cette classe contient toute la logique de gestion du jeu, par exemple les méthodes pour réagir aux interactions de l'utilisateur. Elle contient également les méthodes permettant de tester les collisions. Dans les deux cas, cela amène généralement à modifier le modèle en appelant ses méthodes.

Enfin, elle contient une méthode, appelée régulièrement, qui détecte les collisions, met à jour le modèle, puis la vue.

 

6°/ Classe principale

Comme mentionné en section 4,  la classe principale contenant main() doit hériter de la classe Application, et se faisant, DOIT définir une méthode start()

Dans start(), on procède quasi toujours à l'identique :

  • on instancie le modèle général,
  • on instancie la vue générale (qui contient le Pane racine).
  • on instancie le contrôleur général.
  • on crée un objet Scene, auquel on passe en paramètre le Pane racine.
  • on assigne la Scene au Stage.
  • on affiche le Stage.
  • si besoin, on lance le timer pour commencer l'animation.