Apnée de Programmation sur le dessin avec un Canvas JavaFX

Dans cette apnée, nous allons voir comment il est possible de dessiner assez simplement en JavaFX à l'aide d'un composant appelé Canvas. Nous verrons aussi comment adapter le dessin au nombre de pixels de la fenêtre et comment gérer simplement le redimensionnement. Vous pouvez commencer à partir de votre solution aux précédents TPs et Apnées, ou partir de l'état global Etape5.zip.

0 - Remarque pour les utilisateurs d'éclipse

Attention, par défaut Eclipse se place dans une configuration dans laquelle JavaFX n'est pas accessible. Pour résoudre le problème, deux possibilités :

1 - Motivation : affichage et rafraîchissment

L'interface graphique d'une application est évidemment construite et affichée par l'application elle-même. Dans un système graphique classique, le contenu d'une fenêtre n'est pas sauvegardé par le système. Cela signifie que lorsqu'une fenêtre est redimensionnée, masquée/démasquée par une autre fenêtre ou iconifiée/déiconifiée, il faut que le système graphique puisse avertir l'application qu'elle doit rafraîchir son affichage. Il faut aussi que l'utilisateur ait écrit une méthode capable de redessiner toute la partie de l'affichage qui doit être rafraichie lorsque le système graphique le demande. Ce n'est pas le cas en JavaFX. Pour que la programmation de l'interface soit plus simple, JavaFX masque le processus de rafraichissement et utilisant essentiellement deux techniques :

La seconde technique, l'utilisation d'un Canvas est généralement moins efficace : lorsque le nombre de pixels constituant un canvas est grand, le calcul des lignes tracées par les primitives de dessin et la recopie d'une portion du Canvas dans la fenêtre affichée à l'écran sont lents.

Malgré ses inconvénients, nous utiliserons la technique à base de Canvas pour dessiner l'ensemble de notre jeu à l'écran. La motivation principale est qu'en utilisant un Canvas, nous pourrons écrire un programme dont le style est très proche d'un moteur de rendu classique de jeu vidéo. En effet, un jeu vidéo est, classiquement, articulé autour d'une boucle qui répète les étapes suivantes :

Nous avons donc besoin d'un moyen de redessiner l'ensemble de ce qui doit être affiché à intervalles réguliers (idéalement toutes les 16,5ms, durée en dessous de laquelle la plupart des personnes ne perçoivent plus les saccades). C'est ce qu'offrent les bibliothèques de rendu à bas niveau comme OpenGl, DirectX, SDL ou LWJGL. Nous aurions pu envisager d'utiliser LWJGL qui est (beaucoup) plus efficace que JavaFX, mais nous l'avons écarté pour deux raisons :

Remarque culturelle : avec les progrès technologiques, les cartes graphiques modernes disposent de suffisamment de mémoire pour stocker en permanence le contenu de plusieurs fenêtres (par exemple sur une carte bas de gamme disposant de 1Go de mémoire, on peut stocker environ 128 fenêtres de 1920x1080 pixels). Cette mémoire est exploitée par JavaFX, elle permet à la fois d'accélérer le rendu et d'ajouter facilement des effets, comme la transparence, au contenu des fenêtres. Cela est d'ailleurs l'un des arguments de vente de JavaFX par rapport à Swing (l'interface traditionnelle de java).

2 - Premiers programmes en JavaFX

Le programme ExempleJavaFXAvecImages.java contient un petit exemple d'utilisation de JavaFX pour dessiner des images à l'écran. Une application JavaFX est constituée par une classe qui hérite d'Application dans laquelle on appelle la méthode launch. Cette méthode démarre l'application dans un nouveau thread dedié à JavaFX, ce dernier commence par exécuter la méthode start. Vous pouvez y trouver les composants suivants :

Ce programme d'exemple contient quelques appels de méthodes permettant d'effectuer des taches communes dans une application graphique : Si vous jouez un peu avec l'application, vous pourrez constater qu'en redimensionnant la fenêtre pour la rendre plus petite que le dessin qu'elle contient, vous perdez une partie de ce dessin. En effet, le contenu d'un canvas est stocké comme une image qui est découpée à la taille de celui-ci, si nous voulons que le contenu s'adapte, il faudra le redessiner après un redimensionnement, mais nous verrons cela plus tard.

Exercices

Récupérez le programme Etape6.java. Vous pouvez constater que ce programme ne fait pas grand chose : dans la méthode deroulementJeu il rafraîchit le jeu (pour charger le premier niveau), puis il appelle la méthode creer de la classe InterfaceCanvasJavaFX que vous allez devoir écrire. Dans cette interface il va falloir créer un fenêtre et y dessiner tous les composants du niveau. Pour cela, nous allons naturellement utiliser un visiteur que nous appellerons DessinateurCanvasJavaFXAvecImages.

Vous devriez alors voir l'affichage de votre niveau en minuscule dans le coin supérieur gauche de la fenêtre : le jeu utilise des coordonnées internes qui n'ont potentiellement rien à voir avec la taille en pixels de la fenêtre...

3 - Mise à l'échelle

Nous avons conclu la partie précédente avec un problème de mise à l'échelle de notre dessin. Ce problème n'est pas simple à résoudre :

Pour résoudre notre problème, nous allons ajouter nous même un zoom qui, au moment du dessin, redimensionnera chacun des composants à dessiner en fonction de la taille de la fenêtre, puis leur rendra leur taille originale. En faisant ainsi, nous gagnerons plus tard gratuitement le redimensionnement du jeu lorsque la fenêtre change de taille.

Le parcours des composants du jeu se fait par un visiteur. Si nous voulons mettre à l'echelle chaque composant, il faut donc que ce soit un visiteur qui le fasse. Mais cela ne peut pas être le DessinateurCanvasJavaFXAvecImages : on ne va pas dupliquer le code de redimensionnement dans chaque méthode de visite. Nous allons donc avoir deux visiteurs :

Cette manière de faire ressemble beaucoup à un design pattern appelé décorateur. C'est effectivement le cas, mais si vous l'écrivez comme indiqué ci-dessus, le pattern n'apparaît pas clairement. Nous aurons l'occasion de revenir sur les décorateurs dans les futurs TPs.

Exercices That's all folks.