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 ExempleJavaFXAnime.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 et décorateurs

Certains de nos composants vont maintenant se déplacer à l'écran. Pour cela nous allons les doter d'un moteur : un objet dont le seul rôle sera de changer leur position à intervalles réguliers. La question ici est : quelle forme aura ce moteur ? Nous allons utiliser un nouveau design pattern évoqué lors du dessin, le décorateur :

Définition

Un décorateur prend l'apparence d'un autre objet et lui ajoute des fonctionnalités ou modifie la manière dont ses méthodes s'exécutent.

Particularités

Motivation détaillée pour notre casse brique

Au lieu d'utiliser un décorateur, nous aurions pu envisager d'autres alternatives. Nous pourrions spécialiser la classe ComposantGraphique pour avoir une classe d'objets mouvant dont pourraient hériter la balle, les bonus et la raquette. Mais nous avons décidé de ne pas toucher au fichiers déjà écrits, la balle, les bonus et la raquette héritant déjà de ComposantGraphique, c'est donc impossible. Nous pourrions gérer une liste de moteurs, activés à intervalles réguliers par le timer d'animation. Mais cela limiterait les déplacements possibles à ceux causés par le temps. Plus tard, avec les collisions, cela deviendrait problématique.

Le décorateur va nous permettre d'intégrer notre composant motorisé à la liste des composants du jeu. Il sera traité normalement (pour le dessin, le calcul des collisions, ...), mais le composant sera tout de même doté d'une vitesse, presente dans son décorateur.

3 - Observateurs

Il faut également aborder la question du déclenchement du déplacement des composants via leur moteur. C'est ce que nous allons faire ici en utilisant un design pattern appelé observateur (ou encore publish/subscribe).

Définition

L'observateur (conjointement avec un observable) est une abstraction d'un groupe de composants qu'il est possible de prevenir d'un changement.

Particularités

Motivation détaillée pour notre casse brique

L'observateur intervient pour traiter de manière spécifique le petit nombre de comoposants se déplaçant dans notre jeu. Nous pourrions utiliser un visiteur pour parcourir tous les composants lors du déplacment, mais il serait alors 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 fait l'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.

4 - Exercices

Utilisez les fichiers des précédents TPs et apnées comme point de départ ou récupérez le dernier état global Etape6.zip. Récupérez aussi les fichiers pour ce TP : Animations.zip. Vous pouvez retrouver :

Il ne vous reste plus qu'à compléter, écrivez : et testez l'ensemble avec le programme Etape7.