Apnée de Programmation sur l'animation d'une scène remplie d'objets mouvants

Dans cete Apnée, nous allons voir comment animer un ensemble d'objets se déplaçant à l'écran sous le contrôle d'une application. Notre approche ici est volontairement globale, centrée autour d'une boucle de calcul et rendu, car c'est ainsi qu'un jeu vidéo ou que le moteur interne d'un environnement graphique sont structurés.

1 - Animations en JavaFX

JavaFX propose plusieurs classes pour créer simplement des animations, elle contiennent toutes Transition dans leur nom, elles servent à définir des déplacements, rotation, effets de zoom ou effets d'apparition/disparition sur les objets graphiques proposés par JavaFX : Rectangle, Circle, Ellipse, .... Ces animations peuvent être combinées pour s'enchainer en séquence ou en parallèle et peuvent être placées dans une TimeLine qui permet de les déclencher à un moment précis dans le temps ou de les répéter en boucle. L'ensemble est présenté dans le tutoriel associé sur le site d'Oracle et permet de réaliser très simplement une animation dont le déroulement est connu à l'avance sur des objets graphiques de JavaFX. Malheureusement, ce n'est pas du tout notre cas : nous utilisons un canvas dont le contenu est entièrement dessiné par notre application et les mouvements de nos objets sont calculés à partir des actions de l'utilisateur et du calcul des collisions entre objets. Nous allons donc procéder autrement...

Pour nos animations, nous allons utiliser une méthode à plus bas niveau proposée par JavaFX : un objet de la classe AnimationTimer, une fois démarré (méthode start), appelle de manière régulière sa méthode handle en lui passant le temps courant en nanosecondes (tel que donné également par System.nanoTime()). Le moteur de JavaFX effectue ces appels à une fréquence de 60 appels par secondes s'il y parvient, à une fréquence moindre sinon. Ce mécanisme est exactement ce qu'il nous faut pour un jeu qui doit, idéalement, tourner à 60 images par secondes. Le programme ExempleAnimationTimer.java contient un exemple d'utilisation de cette classe pour animer le dessin donné en exemple au précédent TP. Comme vous pouvez le voir, l'animation est mise en place en créant un objet dont la classe hérite d'AnimationTimer et redéfinit la méthode handle. Dans notre jeu, nos animation consisterons simplement, toutes les 16,5ms, à mettre à jour la position des objets qui se meuvent et à redessiner l'ensemble du niveau.

Remarque : A l'image de l'exemple, nous redessinerons l'intégralité de l'affichage à chaque image produite. C'est ce qui se fait classiquement dans un jeu car cela est souvent nécessaire : l'intégralité de l'affichage change à chaque image lors d'un mouvement de caméra dans un jeu 3D, lors d'un scrolling dans un jeu 2D (shoot'em up, beat'em all, roguelike, ...). Le matériel et les bibliothèque qui vont avec sont donc généralement optimisés pour être utilisés de cette manière.

2 - Objets mouvants

Reprenez l'ensemble des fichiers du précédent TP. Nous allons commencer par mettre en place la gestion de nos objets mouvants. Pour cela nous allons définir une classe ObjetMouvant héritant de ComposantGraphique : l'objet mouvant sera donc un composant graphique particulier. Les classes des objets censés avoir une vitesse (Bonus, Balle et Raquette) doivent donc maintenant hériter d'ObjetMouvant plutôt que de ComposantGraphique.

Exercices

3 - Observateurs

La solution précédente est fonctionnelle, mais il est un peu dommage que l'ensemble des éléments du jeu soient parcourus par le visiteur alors que seulement une petite partie est destinée à être animée. Nous aurons le même problème lorsque nous implémenterons les collisions : seuls les objets mouvants peuvent causer des collisions avec un élément du jeu. Il faudrait donc disposer de la liste des objets mouvants pour la parcourir et n'appeler le visiteur que sur les éléments qu'elle contient. C'est ce que nous allons faire ici en utilisant un design pattern appelé observateur (ou encore publish/subscribe). Dans ce design pattern, un observateur est un objet disposant d'une méthode que nous appellerons miseAJour ne prenant aucun paramètre et permettant juste de l'informer que quelque chose a changé. Il est accompagné d'un objet d'une classe que nous appellerons Observable auquel il est possible d'ajouter un observateur, de supprimer un observateur et qu'il est possible de mettre à jour. La mise à jour de l'observable provoque l'appel de la méthode miseAJour pour tous les observateurs qu'il a enregistré. L'avantage à structurer les choses ainsi est que l'observable n'a pas besoin de connaître les détails de ses observateurs ni de savoir quelles données leur transmettre après une mise à jour : il les prévient et ils viendront chercher eux même les infos directement par l'appel des bonnes méthodes sur l'objet qu'ils observent. Logiquement, ce pattern est très utilisé dans les applications graphiques courantes afin d'éviter de rendre l'application dépendante de son interface graphique. Dans notre cas il nous servira à ne prévenir que les éléments de notre jeu intéressés par un rafraîchissement.

Exercices

4 - Touches

Dans un toolkit graphique, la pression d'une touche sur le clavier (ou son relâchement) est signalée par un évènement. Le programmeur peut réagir à cet évènement en enregistrant auprès du toolkit une méthode, dite de callback qui sera appelée lorsque l'évènement surviendra. Bien entendu, en java, une méthode ne pouvant exister toute seule, c'est un objet qui implémente une interface particulière qui est enregistré. En JavaFX, cette interface est EventHandler. Cette manière de programmer, en enregistrant des callbacks qui seront appelés lors de l'occurrence d'évènements, est appelée programmation évènementielle. L'interface EventHandler est générique, nous la paramètrerons ici par la classe KeyEvent qui correspond aux évènements en provenance du clavier. Quant à l'enregistrement de notre callback, chaque composant graphique présent dans la bibliothèque de JavaFX dispose d'une méthode d'enregistrement pour chaque type d'évènement par lequel il peut être concerné. Le programme ExempleTouches.java illustre cela. Dans ce programme, vous pouvez constater que l'évènement est traité par la méthode handle de l'EventHandler et que le KeyEvent qui lui est passé contient tous les détails concernant l'évènement, ici le code de la touche pressée. En outre, on peut distinguer la pression du relâchement d'une touche car ils sont traités par deux callbacks différents.

Exercices