Faisant suite aux articles sur la façade graphique, je propose à présent d’ajouter un peu d’interaction avec l’utilisation d’une souris. Cela va être l’occasion de voir deux autres patrons: l’Observateur (Observater Pattern) pour gérer les événements de la souris, et la Boucle de Jeu (Game Loop Pattern) pour la synchronisation entre contrôles, mises à jour et affichage.
Façade souris
Pour ajouter la gestion de la souris à la façade, je propose d’introduire une nouvelle interface Mouse
qui contient toutes les fonctions liées à la souris:
Séparer l’interface pour la souris de l’interface générale de la façade a deux avantages: alléger l’interface générale et permettre la gestion simultanée de plusieurs souris.
En ce qui concerne les méthodes, j’ai choisi l’API la plus simple possible:
- Méthode
isButtonPressed()
: renvoietrue
si le boutonbutton
est pressée; - Méthodes
getX()
etgetY()
: renvoient les coordonnées (x,y) du pointeur dans la fenêtre.
Cette API est également bas niveau, afin de correspondre à ce que propose la plupart des bibliothèques graphiques. En outre, cela permet de disposer à tout moment de l’état complet de la souris, ce qui est souvent nécessaire pour les jeux vidéo.
Pour la façade générale GUIFacade
, une nouvelle méthode getMouse()
est ajoutée pour renvoyer une implantation de l’interface Mouse
.
Gestion de la souris avec AWT
Pour implanter cette interface, je reste sur la bibliothèque AWT incluse dans la bibliothèque standard Java. Celle-ci propose une API haut niveau, qui correspond aux besoins des applications de bureautique. Elle est basé sur le patron Observateur (Observater Pattern), qui permet à un élément d’observer (ou écouter) un autre élément, et d’être averti (ou notifié) lorsqu’un événement se produit. Dans le cas de la souris, ces événements sont par exemple la pression d’un bouton ou le déplacement du curseur.
Cette API est divisée en plusieurs interfaces, par exemple l’interface MouseListener
permet de gérer les événements liés aux boutons:
L’interface
MouseListener
contient les méthodes implantées par l’élément observateur: par exemple, lorsqu’un bouton est pressé, la méthode mousePressed()
est appelée et l’observateur peut alors agir en conséquence. La classe Component
, mère de nombreux éléments graphiques au sein d’AWT, peut être observée par quiconque le demande via la méthode addMouseListener()
. Pour l’exemple de cet article, c’est le Canvas
utilisé pour rendre le niveau du jeu qui est observé: chaque action de la souris dans sa zone d’affichage provoque des appels aux méthodes de l’interface MouseListener
.
Implantation AWT
Je propose que l’implantation de l’interface Mouse
de la façade prenne la forme d’une classe AWTMouse
:
La classe implante les interfaces
Mouse
, MouseListener
et MouseMotionListener
. Dans le premier cas, elle fournit des informations sur la souris (contenues dans ses attributs):
public class AWTMouse implements Mouse, MouseListener, MouseMotionListener { private final boolean[] buttons; private int x; private int y; public AWTMouse() { buttons = new boolean[4]; } @Override public boolean isButtonPressed(int button) { if (button >= buttons.length) { return false; } return buttons[button]; } @Override public int getX() { return x; } @Override public int getY() { return y; } ...
Dans les cas suivants, elle réagit aux événements de la souris pour mettre à jour les informations de la souris:
@Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { if (e.getButton() <= 3) { buttons[e.getButton()] = true; } } @Override public void mouseReleased(MouseEvent e) { if (e.getButton() <= 3) { buttons[e.getButton()] = false; } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseDragged(MouseEvent e) { x = e.getX(); y = e.getY(); } @Override public void mouseMoved(MouseEvent e) { x = e.getX(); y = e.getY(); } }
Enfin, la classe AWTWindow
contient comme précédemment le canvas, et celui-ci est utilisé pour “écouter” les événements de la souris:
public void init(String title, int width, int height) { ... mouse = new AWTMouse(); canvas.addMouseListener(mouse); canvas.addMouseMotionListener(mouse); }
Il est possible d’effectuer un travail similaire pour le clavier: pour l’interface, l’important et de permettre à l’utilisateur de disposer à tout moment de l’état du clavier, et pour l’implantation AWT, le clavier est également géré avec le parton Observateur.
Boucle de Jeu
Il ne reste plus qu’à exploiter cette nouvelle interface dans un exemple. Pour ce faire, je propose d’introduire le patron Boucle de Jeu (Game Loop Pattern) dans sa version la plus simple (sans considérations multi-thread). Il repose tout d’abord sur un ensemble de méthodes que l’on peut regrouper au sein d’une même classe:
- La méthode
init()
est appelée au démarrage pour initialiser le jeu et ses données; - La méthode
processInput()
est appelée à chaque itération du jeu pour gérer les contrôles (clavier, souris, …). En deux mots, son principal rôle consiste à transformer des “instructions” de l’utilisateur en des “ordres” ou “commandes” plus abstraites que le moteur de jeu sait interpréter. Sur le plan temporel, ces opérations vont au rythme de l’utilisateur. - La méthode
update()
applique les changements sur les données du jeu en fonction de diverses sources, comme les commandes produites par les contrôles utilisateurs ou des opérations qui doivent être appliquées à chaque mise à jour. Sur le plan temporel, ces opérations vont à la vitesse du moteur de jeu. - La méthode
render()
fait le nécessaire pour que l’affichage soit correct. En pratique, cette méthode va le plus souvent commander ou transférer des données sur la carte graphique, cette dernière s’occupant des tâches très bas niveau, au niveau des pixels. Sur le plan temporel, ces opérations vont à la vitesse de rafraîchissement de l’écran (par défaut 60Hz). - La méthode
run()
appelle les précédentes et contient la boucle de jeu à proprement parlé.
Pour notre illustration, je n’ai pas encore mis en place tous les éléments qui permettent de respecter tous ces principes. Je propose donc ici une exploitation de ce patron avec une forme pas totalement orthodoxe, mais suffisante, enfin je l’espère, pour commencer à comprendre toutes ces notions.
Pour la méthode init()
, on fabrique et paramètre les deux calques (layers) et initialise la fenêtre:
public void init(Level level) { this.level = level; backgroundLayer = gui.createLayer(); backgroundLayer.setTileSize(level.getTileWidth(),level.getTileHeight()); backgroundLayer.setTexture(level.getTilesetImage(0)); backgroundLayer.setSpriteCount(level.getWidth()*level.getHeight()); groundLayer = gui.createLayer(); groundLayer.setTileSize(level.getTileWidth(),level.getTileHeight()); groundLayer.setTexture(level.getTilesetImage(1)); groundLayer.setSpriteCount(level.getWidth()*level.getHeight()); gui.createWindow("Exemple de contrôle avec la souris", scale*level.getTileWidth()*level.getWidth(), scale*level.getTileHeight()*level.getHeight()); }
Pour la méthode processInput()
, si le bouton gauche est pressé, qu’il y a une cellule du niveau sous le curseur, et que sa tuile est dans le deuxième jeu de tuiles (celui du second calque), alors on y place une tuile avec de l’herbe:
public void processInput() { Mouse mouse = gui.getMouse(); if (mouse.isButtonPressed(MouseEvent.BUTTON1)) { int x = mouse.getX() / (scale*level.getTileWidth()); int y = mouse.getY() / (scale*level.getTileHeight()); if (x >= 0 && x < level.getWidth() && y >= 0 && y < level.getHeight()) { if (level.getTileset(x,y) == 1) { level.setTileset(x,y,0); level.setTile(x,y,new Point(7,0)); } } } }
Pour la méthode update()
, le contenu du niveau est utilisé pour définir les textures à utiliser pour rendre le niveau. C’est la même chose que dans l’article précédent, sinon le fait que cette opération est répétée régulièrement:
public void update() { for(int y=0;y<level.getHeight();y++) { for(int x=0;x<level.getWidth();x++) { int index = x + y * level.getWidth(); backgroundLayer.setSpriteLocation(index, new Rectangle(scale*x*level.getTileWidth(), scale*y*level.getTileHeight(), scale*level.getTileWidth(), scale*level.getTileHeight())); if (level.getTileset(x, y) == 0) { Rectangle tile = new Rectangle(level.getTile(x, y), new Dimension(1,1)); backgroundLayer.setSpriteTexture(index, tile); } else { backgroundLayer.setSpriteTexture(index, null); } } } for(int y=0;y<level.getHeight();y++) { for(int x=0;x<level.getWidth();x++) { int index = x + y * level.getWidth(); groundLayer.setSpriteLocation(index, new Rectangle(scale*x*level.getTileWidth(), scale*(y-1)*level.getTileHeight(), scale*level.getTileWidth(), scale*2*level.getTileHeight())); if (level.getTileset(x, y) == 1) { Rectangle tile = new Rectangle(level.getTile(x, y), new Dimension(1,2)); groundLayer.setSpriteTexture(index, tile); } else { groundLayer.setSpriteTexture(index, null); } } } }
La méthode render()
se contente d’effectuer l’affichage, à savoir dessiner les deux calques:
public void render() { if (gui.beginPaint()) { gui.drawLayer(backgroundLayer); gui.drawLayer(groundLayer); gui.endPaint(); } }
Enfin, la méthode run()
contient la boucle de jeu qui appelle les autres méthodes au maximum 60 fois par seconde:
public void run() { int fps = 60; long nanoPerFrame = (long) (1000000000.0 / fps); long lastTime = 0; while(!gui.isClosingRequested()) { long nowTime = System.nanoTime(); if ((nowTime-lastTime) < nanoPerFrame) { continue; } lastTime = nowTime; processInput(); update(); render(); long elapsed = System.nanoTime() - lastTime; long milliSleep = (nanoPerFrame - elapsed) / 1000000; if (milliSleep > 0) { try { Thread.sleep (milliSleep); } catch (InterruptedException ex) { ex.printStackTrace(); } } } gui.dispose(); }
Cette implantation simple du patron Boucle de Jeu ne respecte pas tous ses principes. Il n’y a pas de véritable notion de commande, et il sera difficile de faire évoluer chaque partie à des rythmes différents. En outre, en terme de conception, il y a aussi des choix discutables, comme la classe Level
sert à la fois de chargeur de niveau, stockage pour le niveau, et d’une forme de tampon. Par l’intermédiaire de prochains articles, je vous présenterai petit à petit tout ce qu’il faut pour parvenir à un résultat satisfaisant avec toutes les propriétés qui en découlent.
Le code de cet article peut être téléchargé ici.
Pour compiler, saisissez: javac fr/phtools/awtfacade05/Main.java
Pour lancer, saisissez: java fr.phtools.awtfacade05.Main