|
TD6 - Etude d'un programme utilisant des plugins
De $1
Introduction
Il s'agit du dernier "vrai TP", aujourd'hui vous allez juste vous familiariser avec une version du TP précédent (le jeu vidéo 2D) mais qui utilise un système de plugins pour charger des comportements. Avant toute choses, vous allez récupérer les différents éléments : le projet du jeu lui même, un autre projet pour développer les plugins de comportement, et un troisième projet (un peu compliqué) qui contient une librairie de gestion de plugins que vous pourrez réutiliser dans vos propres projets.
Récupération des projets
Les trois projets existent pour Netbeans et pour Eclipse. Les source sont les mêmes.
Archive des trois projets Eclipse : EclipseProjectsPlugins.zip
Archive des trois projets Netbeans : NetBeansProjectsPlugins.zip
Dezippez les trois projets Eclipse dans votre workspace ou les trois projets Netbeans dans l'endroit où vous stockez les projets Netbeans (par défaut : Mes Documents/Netbeans projects, mais pour Netbeans cela n'a pas tellement d'importance, il sait ouvrir des projets n'importe où).
Il se peut que vous ayez des warnings, ignorez les. Vous devriez obtenir dans Eclipse ceci :

Exécution du jeu
Vous allez maintenant exécuter le projet du jeu. Par défaut aucun plugins n'est installé dans le sous-répertoire "plugins" du projet. Il est vide.

Si tout s'est bien passé, vous devriez obtenir une fenêtre avec un Cercle (pas encore animé !). Pour le moment aucun comportement n'est associé à ce cercle, donc il ne bouge pas. Le menu plugins (qui permettra justement d'ajouter ou d'enlever des comportements au cercle) est vide. On peut voir dans la console la trace du chargement des plugins qui indique clairement qu'aucun plugin n'a été trouvé. Les plus curieux peuvent regarder la classe Fenetre.java car c'est là que tout se passe...

Arrêtez le programme. Nous allons maitenant copier des plugins dans le répertoire "plugins", et relancer l'application. Pour cela, le projet MiniJeu1Plugins vient avec un jar qui contient 4 plugins de comportement. Nous allons donc depuis l'interface d'Eclipse, copier le jar du projet MiniJeu1Plugins vers le répertoire "plugins" du projet MiniJeu1 :

Et on le colle au bon endroit :

Remarque : Le fichier .jar contient toutes les classes du projet des plugins, soit l'ensemble des comportents et leurs dépendances (qui sont dans le package pluginsSDK). Mettre des plugins (des classes chargées à la volée par un Class.forName ou à l'aide d'un UrlClassLoader, comme nous l'avons vu en TP) dans un jar est effectivement très pratique pour les déployer dans un projet qui est capable de les charger.
Relancez l'application. Vous devriez voir maintenant 4 plugins dans le menu. Commencez par ajouter au Cercle le comportement "rebondir", puis "par defaut", le cercle doit avancer et rebondir. Vous pouvez ensuite ajouter le comportement "ivre", le retirer, ajouter le comportement "suivre souris", le retirer, etc..
Nous allons maintenant supprimer un plugin, pour cela il suffit de modifier une classe du package des comportements, ici ComportementSuitSouris.java, pour ne plus qu'elle implémente ComportementPlugin.java. Ainsi, notre classe n'est plus un Plugin de comportement.

Pour faire un jar avec Eclipse, c'est un peu plus compliqué qu'avec Netbeans (oui, M.Buffa aime bien Netbeans, vous l'avez compris...) : il faut utiliser le menu "Export" de Eclipse. Suivez le guide !


Une fenêtre apparait, là vous allez devoir utiliser le bouton Browse pour choisir l'emplacement et le nom du fichier .jar qui sera produit. J'ai choisi de le mettre à la racine du projet et de lui donner le nom du projet terminé par .jar. Attention, si vous avez déjà fait un jar et que vous voulez re-exporter le projet dans un jar, cet idiot d'Eclipse propose par défaut de remettre le jar dans le jar (cf photo), il faut décocher le jar dans la partie en haut à droite...

Cliquez surt Finish et vous devriez obtenir un fichier jar mis à jour. Supprimez celui qui est situé dans le répertoire plugins du Jeu, puis recopiez le nouveau à la place. Relancez le jeu, vous ne devriez avoir plus que trois plugins.
Etude du code, de la conception
Projet MiniJeu1
Lorsque il a fallu transformer le TP du jeu pour mettre en évidence une conception à base de plugins, il a fallu refactorer le projet. Pourquoi ? Parceque dans l'hypothèse où un groupe développe le jeu et où d'autres groupes développent les plugins, il faut identifier quelles seront les classes qui seront nécessaires à l'écriture de plugins. On les a mises dans le package pluginsSDK. Vous remarquerez que la classe Jeu.java y figure, simplement, on ne "construit" plus le jeu dans cette classe, regardez là et comparez avec la version de l'ancien TP. Le constructeur ne crée plus d'objets...er
En revanche la classe MaFenetre.java qui construit la GUI du jeu a légèrement changé. Elle crée la fenêtre principale, instancie le Jeu (qui est un JPanel) et le met au centre, puis ajoute un Cercle dans le jeu. Ensuite on utilise les classes PluginManager et PluginLoader de la librairie de gestion des plugins pour :
- Charger les classes de Plugins dans un tableau de plugins (toutes celles qui sont des Plugin)
- Instancier celles qui sont des ComportementPlugins (rappel, si vous regardez le code, ComportementPlugin implémente Plugin)
- Créer un menu déroulant avec des CheckBoxMenuItems pour chaque comportement, associer un ActionListener à chaque entrée pour ajouter/enlever le comportement sélectionné au cercle.
Etudions plus en détail... tout se passe dans l'appel de la méthode :
- private void litPluginsEtConstruitMenuDesComportements(final JMenuBar mb) {
-
- pluginManager = PluginManager.getPluginManager();
-
- try {
-
- pluginManager.addJarURLsInDirectories(new URL[]{new URL("file:plugins")});
- } catch (MalformedURLException ex) {
- Logger.getLogger(MaFenetre.class.getName()).log(Level.SEVERE, null, ex);
- }
-
-
- pluginManager.loadPlugins();
-
-
-
- plugins = (ComportementPlugin[]) pluginManager.getPluginInstances(ComportementPlugin.class);
-
-
-
-
- buildPluginMenu(mb);
- }
private void litPluginsEtConstruitMenuDesComportements(final JMenuBar mb) {
// Ici on lit les plugins et on construit le menu
pluginManager = PluginManager.getPluginManager();
try {
// Tous les jars dans le sous dir plugins seront lus
pluginManager.addJarURLsInDirectories(new URL[]{new URL("file:plugins")});
} catch (MalformedURLException ex) {
Logger.getLogger(MaFenetre.class.getName()).log(Level.SEVERE, null, ex);
}
// On charge les classes qui oimplémentent l'interface Plugin.class. Voir le package pluginsSDK
pluginManager.loadPlugins();
// Ici on crée un tableau avec une instance de chaque plugin. On s'en sert un peu plus bas pour
// construire un menu de comportements
plugins = (ComportementPlugin[]) pluginManager.getPluginInstances(ComportementPlugin.class);
// On construit le menu nous indiquant les comportements possibles
// Dans cette méthode on ajoute un écouteur à chaque menuItem
// qui associe un comportement/plugin au cercle créé dans le jeu
buildPluginMenu(mb);
}
Une fois le tableau des ComportementPlugins rempli avec une instance de chaque comportement, on peut construire le menu. Pour cela on utilise les classes PluginMenuFactory.java et PluginMenuItem.java qui nous facilitent la vie.
- private void buildPluginMenu(JMenuBar menuBar) {
- menuPlugins = new JMenu("Plugins");
-
-
-
- ActionListener listener = new ActionListener() {
-
- public void actionPerformed(ActionEvent e) {
-
- JCheckBoxMenuItem cb = (JCheckBoxMenuItem) e.getSource();
-
-
-
- String nameCommand = cb.getActionCommand();
-
-
- ComportementPlugin c = searchPlugin(nameCommand);
-
-
-
-
-
- if (cb.getState() == true) {
- System.out.println("J'ajoute le " + c.getName());
- objetAnime.ajouteComportement(c);
- } else {
- System.out.println("Je supprime le " + c.getName());
- objetAnime.supprimeComportement(c);
- }
- }
- };
-
-
- if (pluginMenuItemFactory == null) {
-
-
- pluginMenuItemFactory = new PluginMenuItemFactory(menuPlugins, pluginManager, listener);
-
- }
-
- buildPluginMenuEntries();
-
-
- menuBar.add(menuPlugins);
-
-
- }
-
- private void buildPluginMenuEntries() {
-
-
- pluginMenuItemFactory.buildMenu(null);
- }
private void buildPluginMenu(JMenuBar menuBar) {
menuPlugins = new JMenu("Plugins");
// L'actionListener qui va écouter les entrées du menu des plugins. C'est une instance d'une
// classe interne anonyme...
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
// On récupère le menu item qui a été choisi
JCheckBoxMenuItem cb = (JCheckBoxMenuItem) e.getSource();
// On récupère la chaine qui était affichée dans le menu,
// c'est par défaut dans l'attribut ActionCommand du menu item
String nameCommand = cb.getActionCommand();
// On va chercher dans le tableau des plugins celui qui porte le même nom
ComportementPlugin c = searchPlugin(nameCommand);
// On regarde si il est selectionné, si il est selectionné c'est que
// l'objet avait déjà ce comportement, dans ce cas on le retire,
// sinon on l'ajoute. Dans tous les cas, on inverse l'état du
// checkbox menu item
if (cb.getState() == true) {
System.out.println("J'ajoute le " + c.getName());
objetAnime.ajouteComportement(c);
} else {
System.out.println("Je supprime le " + c.getName());
objetAnime.supprimeComportement(c);
}
}
};
if (pluginMenuItemFactory == null) {
// Utilisation d'une factory pour construire les menus à partir du tableau de plugins
// Remarque : le dernier paramètre est l'écouteur qui sera exécuté lorsque le menu item sera selectionné
pluginMenuItemFactory = new PluginMenuItemFactory(menuPlugins, pluginManager, listener);
}
buildPluginMenuEntries();
// On rajoute le menu à la barre de menus
menuBar.add(menuPlugins);
}
private void buildPluginMenuEntries() {
// Fait construire les entrées du menu des plugins
pluginMenuItemFactory.buildMenu(null);
}
Projet MinJeu1Plugins
Dans ce projet on retrouve les comportements du TP précédent, à quelques différences près :
- Les noms des packages ont changé,
- Une classe ComportementPlugin a fait son apparition, tous les Comportements vont l'étendre. Cette classe implémente la classe Plugin qui vient de la librairie de plugins. Elle oblige tous les plugins à implémenter des méthodes getName(), getDescription(), getVersion() c'est-à-dire qu'elle oblige tous les plugins à se décrire. Elle oblige aussi à implémenter canProcess(Object o) et une autre méthode, nous en verrons l'utilité plus tard. Pour le moment, lorsque vous les implémenterez elle renverront true. (Pour info : elles servent à dire si un plugin donné peut traiter un autre type d'objet).
- package minijeu.pluginsSDK;
-
- import fr.unice.plugin.Plugin;
-
- public interface ComportementPlugin extends Plugin {
-
- public void deplace(ObjetAnime o);
-
-
- }
package minijeu.pluginsSDK;
import fr.unice.plugin.Plugin;
public interface ComportementPlugin extends Plugin {
public void deplace(ObjetAnime o);
}
Et voici par exemple le comportement par défaut :
- package minijeu.comportements;
-
- import minijeu.pluginsSDK.ComportementPlugin;
- import minijeu.pluginsSDK.ObjetAnime;
-
- public class ComportementParDefaut implements ComportementPlugin {
-
- public void deplace(ObjetAnime o) {
-
-
-
-
- o.xPos += o.vitesse * Math.cos( o.direction);
- o.yPos += o.vitesse * Math.sin( o.direction);
- o.vitesse = o.vitesse + o.acceleration;
- o.direction = o.direction + o. accelerationAngulaire;
-
-
- o.normaliseDirection();
-
- }
-
-
- public String getName() {
- return "Comportement par defaut";
- }
-
- public Class getType() {
- return ComportementParDefaut.class;
- }
-
- public String getDescription() {
- return "Comportement par defaut : l'objet avance dans sa direction, à sa vitesse...";
- }
-
- ...
-
- public String getVersion() {
- return "1.0";
- }
- }
package minijeu.comportements;
import minijeu.pluginsSDK.ComportementPlugin;
import minijeu.pluginsSDK.ObjetAnime;
public class ComportementParDefaut implements ComportementPlugin {
public void deplace(ObjetAnime o) {
// le comportement de base, fait avancer l'objet dans sa direction,
// en fonction de sa vitesse linéaire. Il fait aussi varier sa vitesse linéaire
// et angulaire en fonction des accélérations linéaires et angulaires
o.xPos += o.vitesse * Math.cos( o.direction);
o.yPos += o.vitesse * Math.sin( o.direction);
o.vitesse = o.vitesse + o.acceleration;
o.direction = o.direction + o. accelerationAngulaire;
// On garde la direction entre 0 et 2*PI
o.normaliseDirection();
}
public String getName() {
return "Comportement par defaut";
}
public Class getType() {
return ComportementParDefaut.class;
}
public String getDescription() {
return "Comportement par defaut : l'objet avance dans sa direction, à sa vitesse...";
}
...
public String getVersion() {
return "1.0";
}
}
Projet libPlugins
Ce projet est assez compliqué, en plus sous Eclipse, tous les accents dans les commentaires ont été transformés en trucs bizarre suite à des problèmes d'encodage (ça marche dans Netbeans)... Regardez surtout les classes Plugin.java, PluginLoader.java, PluginManager.java
On ne vous demande pas de tout comprendre dans le détail (à propos, ce projet utilise des enums qui ont changé depuis java 1.3, il a été écrit à l'époque de java 1.3, avant que cela change, c'est pourquoi dans les propriétés du projet nous sommes dans un mode de compatibilité java 1.3). Notez cependant que gérer tous les cas de figures (plugins dans un jar, dans plusieurs jars, hors des jars, filesystem unix ou windows, nombreux cas d'erreurs gérés par de très nombreuses exceptions, etc) représente pas mal de travail.
Cette librairie a été écrite par Richard Grin et pas mal modifiée par Michel Buffa il y a quelques années. Vous pouvez la réutiliser telle quelle dans vos projets.
Travail à faire
- Ecrire un plugin "tout en un", un ComportementQuiRebonditEtQuiSuitLaSouris
- Réfléchir à comment écrire un autre type de plugin, par exemple, des plugins "niveaux" qui lorsqu'on les invoque créent plusieurs objets avec des comportements (un peu comme dans le TP précédent). Vous remarquerez que la méthode qui sert à créer les instances des plugins (voir MaFenetre.java) permet de spécifier le type des plugins que l'on veut instancier :
-
-
- plugins = (ComportementPlugin[]) pluginManager.getPluginInstances(ComportementPlugin.class);
// Ici on crée un tableau avec une instance de chaque plugin. On s'en sert un peu plus bas pour
// construire un menu de comportements
plugins = (ComportementPlugin[]) pluginManager.getPluginInstances(ComportementPlugin.class);
Imaginez que l'on veuille faire un second menu "Objets" avec des Cecles, des Rectangles, etc... Ajoutez au projet des plugins une classe ObjetPlugin.java, et faites des sous-classes pour créer un ou plusieurs autres types d'objets, peut etre avec des Comportements (note : les plugins objet auront accès au plugins comportement puisque dans le même projet). Faites en sorte que 1) un menu des objets se remplisse, 2) qu'en sélectionnant une entrée cela crée un objet correspondant dans le jeu (vous lui donnerez une position aléatoire pour démarrer).
A LIRE AVANT DE FAIRE LE TRAVAIL :
- Les plugins doivent avoir un constructeur par défaut sans paramètres, en effet la librairie de plugins, lorsqu'elle créée des instances, fait juste un appel à la méthode newInstance() de la classe du plugin.
- Chaque plugin doit renvoyer dans la methode matches(type, name, object) true si le plugin est du type demandé lors de l'appel à pluginManager.getInstances. C'est également ce type qui sera celui des éléments du tableau d'instance rendu. Par exemple, si j'ai un type de plugins ObjetPlugin.java qui est une interface qui étends Plugin. Si j'ai ObjetAnime.java qui étend cette classe (et qui est donc un ObjetPlugin), et que j'ai Cercle qui étend ObjetAnime, je dois déclarer mon tableau de plugins comme ObjetAnime[], faire le getInstances en demandant des ObjetAnime.class et la méthode matches() de Cercle doit renvoyer (type == ObjetAnime.class).
CORRECTION qui propose un menu d'objetPlugins, pour voir comment gérer deux menus
- MiniJeu1EtPluginsCorrection.zip : attention, projet netbeans seulement (mais ne prenez que les sources). Montre comment on gère deux menus et deux types de plugins différents.
|