FMIN205 Rapport TER : Bounding Balls FrameWork du Master 1
Transcription
FMIN205 Rapport TER : Bounding Balls FrameWork du Master 1
FMIN205 Rapport TER : Bounding Balls FrameWork du Master 1 Informatique Participants : Galais Sami Gendre Samuel Giachetto Frédéric Sauvajol Guillaume Tuteur : Dony Christophe 1 Table des matières 1 Introduction - Cahier des Charges 1.1 Rappel du sujet . . . . . . . . . . . 1.2 Analyse du code et tâches . . . . . 1.3 Répartition du travail . . . . . . . 1.4 Récapitulatif du travail effectué . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Le Framework 2.1 Le framework fourni . . . . . . . . . . . . . . . . . . . 2.1.1 Le fonctionnement de base . . . . . . . . . . . 2.1.2 Diagramme de classe simplifié du framework de 2.1.3 Schéma de classe d’un jeu . . . . . . . . . . . . 2.2 Les améliorations apportées . . . . . . . . . . . . . . . 2.2.1 Animations des Entity . . . . . . . . . . . . . . 2.2.2 L’ajout du son . . . . . . . . . . . . . . . . . . 2.2.3 La gestion de la souris . . . . . . . . . . . . . . 2.2.4 L’éditeur de niveau générique . . . . . . . . . . 2.2.5 Diagramme de classe final simplifié . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 3 4 . . . . . . base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 6 7 7 7 9 10 10 13 . . . . . . . . 3 La création d’un jeu 14 3.1 Les classes à créer . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3.2 L’utilisation du pattern state . . . . . . . . . . . . . . . . . . . . 14 3.3 Schéma de classe du casse-brique . . . . . . . . . . . . . . . . . . 15 4 Conlusion 16 2 Chapitre 1 Introduction - Cahier des Charges 1.1 Rappel du sujet Un précédent groupe a réalisé un framework en Java permettant la création de jeux type ’objet mobiles-obstacles-collisions’. Le but du TER est donc d’analyser le code, de reprendre le framework et de l’étendre. Par conséquent il nous faudra développer un premier jeu à partir de ce framework de base. Après cette première étape, il sera nécessaire d’étendre le framework en y intégrant les fonctions qui nous apparaissent indispensables après l’implémentation du premier jeu. 1.2 Analyse du code et tâches Ayant déjà fait une première lecture du code et regardé les jeux fournis avec le framework, nous avons décidé de diviser le travail en plusieurs étapes : – tout d’abord nous allons commencer par implémenter un premier jeu afin de bien comprendre l’utilisation du framework. – ensuite voir les extensions possibles du framework. (graphisme/sons/souris ...) – enfin développer de nouveaux jeux en utilisant les nouvelles fonctionnalités implémentées. 1.3 Répartition du travail Afin d’optimiser notre travail, et donc de pouvoir finir à temps l’ensemble de ce que nous souhaitons produire nous avons décidé de répartir le travail comme suit. Dans un premier temps nous allons développer chacun de notre coté un premier jeu simple « falldown », afin que tout le monde est pris connaissance du framework et puisse ainsi travailler dessus sans soucis. Ensuite nous distribuerons les différents ajouts à effectuer sur le framework à chacun en fonction de nos préférences. Ainsi un membre du groupe s’occupera de développer la partie 3 graphisme, un de la gestion de la souris, un autre implémentera le son et le dernier de l’ajout de l’éditeur générique. Après ce travail effectué nous travaillerons ensemble sur le développement du premier jeu, afin que chacun puisse voir le travail des autres. Pour faciliter la communication entre les différents membres du groupe nous utiliserons bien sur les mails, nous avons aussi mis en place un groupe de discussion à l’adresse http ://groups.google.fr/group/terbbfw afin de faciliter les mises à jour du code. Enfin, en fonction de nos emplois du temps, nous nous réunirons au moins une fois par semaine à la bibliothèque et si cela n’est pas possible nous le ferons via IRC ou MSN. 1.4 Récapitulatif du travail effectué Après la réalisation d’un premier jeu rapide, nous avons commencé à modifier le framework en groupe. Le premier ajout sur lequel nous avons travaillé fut assez simple, et permet au framework de charger une image de fond pour les niveaux. Après cela nous avons travaillé sur le chargement et l’affichage d’images animées (plus communément appelées sprites) à la place des formes simples de java. 4 Chapitre 2 Le Framework 2.1 2.1.1 Le framework fourni Le fonctionnement de base Le framework qu’on nous a demandé d’améliorer est un framework de jeu qui charge un jar contenant un jeu écrit pour l’utiliser. Sa réalisation suit le schéma de conception MVC (Modèle Vue Contrôleur) qui permet de séparer l’affichage graphique et la représentation des données. Dans la partie « model » on trouve notamment la classe GameManager qui contrôle la partie à chaque tour d’horloge, et appelle toute les autres classes. On trouve aussi toutes les sous classes de Entity qui définissent les diverses entités d’un jeu. (celles qui sont statiques : StaticEntity, celles qui peuvent se déplacer : MovableEntity, celle qui sont contrôlables : ControllableEntity ...) Dans la partie « view » on trouve la classe qui gère l’affichage des entités Gentity. Cette classe possédant bien sûr un attribut model de type Entity ! Il va de soi que chaque jeu écrit pour utiliser ce framework devra surcharger toutes les classes citées au dessus. 5 2.1.2 Diagramme de classe simplifié du framework de base Fig. 2.1 – cf. http ://groups.google.fr/group/terbbfw pour les diagrammes complets 6 2.1.3 Schéma de classe d’un jeu Fig. 2.2 – Diagramme d’un jeu 2.2 2.2.1 Les améliorations apportées Animations des Entity Nous avons intégré au framework la gestion d’images animées qui n’était pas présente à l’origine. Pour cela nous avons dû modifier des classes existantes et en créer de nouvelles. Nous avons commencé par définir une classe Sprite qui permet simplement de stocker une image, avec ses dimensions et ses coordonnées ; une méthode draw(Graphics 2D) qui est appelée pour dessiner l’image en fonction des attributs de la classe. C’est en fait la classe de base pour instancier et dessiner une image qui fait partie d’une animation. La hiérarchie du framework est faite de telle sorte qu’une classe abstraite Entity représente les entités des jeux. Quatre autres classes abstraites en héritent : – MovableEntity qui représente les entités mouvantes, mais non controllables par un joueur – ControllableEntity pour les entités qui peuvent se mouvoir et également controllables – StaticEntity pour les entités immobiles, typiquement des briques – enfin BonusEntity pour les bonus. 7 Le moteur de jeu possède une liste pour chaque type d’entité : une méthode qui détermine les collisions entre elles et appelle une méthode collide() dans l’entité pour déterminer les effets de la collision. Les entités possèdent donc chacune quatre méthodes collide(). Pour gérer des animations, nous avions tout d’abord pensé créer une classe AnimatedEntity qui heriterait de Entity . L’héritage multiple étant inexistant en java, il nous fallait une classe héritant de AnimatedEntity par type d’entité (AnimatedControllableEntity ...) en plus, bien sûr, des classes d’entités existantes. Cela aurait posé quelques problèmes. Tout d’abord, dans les classes animées, il y aurait eu systématiquement le même code que dans l’homologue non animé. Les autres problèmes seraient venus du fonctionnement du framework lui même. En effet, le GameEngine aurait comporté non pas quatre, mais huit listes d’entités. Par exemple, il n’y aurait plus eu d’héritage entre une ControllableEntity et une AnimatedControllableEntity, ce qui veut dire deux listes différentes. Un autre gros problème aurait été posé par les méthodes collide() des entités et par la méthode de détection des collisions du GameEngine. De fait, trop de cas aurait dû être déterminé, soixante-quatre au total, et tous avec un code similaire. Nous avons donc opté pour une autre solution, c’est-à-dire, définir une classe ” Animated ” par entité héritant directement de cette entité. Cela résout le problème des listes du GameEngine et celui des méthodes de collision. Même si le code de ces classes est semblable, cette solution est bien meilleure que la précédente envisagée. Le fonctionnement du système d’animation est quant à lui assez simple : il repose sur un système de ” mode ”, facilement implémentable grâce à un pattern state. Les classes possèdent une table de hachage avec des clés String, qui correspondent au ” mode ”, et une liste de Sprite comme valeur qui constitue l’animation. Chaque ” mode ” a donc une animation particulière. Un attribut currentImage correspond à l’image courante à afficher dans le mode courant. Une méthode advance() appelée à chaque tick, ou rafraichissement de la scène, fait passer par défaut currentImage au Sprite suivant dans la liste. Il est possible de ralentir l’animation en changeant la valeur de l’attribut entier step. Il correspond au nombre de ticks à laisser passer pour avancer au Sprite suivant. Sa valeur par défaut est de zéro. Toutes ces actions se font de manière transparente ; l’utilisateur du framework n’aura pas besoin de les coder, mis à part s’il désire, par exemple, faire d’autres choses dans la méthode advance(). Il lui suffira alors de la redéfinir, en pensant à appeler super.advance(). Le chargement des images se fait grâce à la méthode createImageList() qui est appelée directement par le constructeur de la classe. Elle commence par charger le chemin absolu vers le dossier du jeu chargé, puis rentre dans le dossier Sprites. Là, elle cherche un dossier qui porte le nom de la classe appelante et le parcourt. Les dossiers qu’elle rencontre correspondent à des ”modes ”. Leur nom sert de clé dans la HashMap et les images contenues dans le dossier sont associés à cette clé . Cela impose quelques contraintes pour que les images et a fortiori les animations soient bien chargées. Tout d’abord, le .jar du jeu doit se trouver dans un dossier qui contient également un dossier ” Sprites ”. A l’intérieur, on doit trouver un dossier par classe animée contenant lui-même un dossier par ” mode ” de la 8 classe, avec les images correspondantes dedans. Finalement, la contrainte reste bien maigre puisque le chargement des images se fait réellement de manière automatique.L’utilisateur n’a rien besoin de coder pour le faire. Il lui faut simplement déterminer les modes de son entité et les images associées. Dans le code, il doit tout de même déterminer les changements de modes qui interviennent souvent suite à une collision ou un déplacement, par exemple. La part de codage est minimisée et le résultat est garanti. Il est très facile de changer complètement l’apparence d’un jeu juste en changeant les images dans les dossiers. 2.2.2 L’ajout du son Il s’agissait ici de parvenir à integrer une solution simple au framework pour jouer des sons associés aux évenements du jeu ainsi qu’une musique de fond. Cela c’est fait en deux temps : Tout d’abord nous avons créer une classe Sound dans le package util. Celle-ci possède cinq attributs : – soundFile qui correspond au fichiers .wav à jouer – soundStream qui est le stream associé au son – soundFormat, le format du son – soundInfo, le DataLine.info du son – soundClip le clip associé au sons, c’est lui qui est joué par Java. Cette classe implemente les méthodes play(), pause(), stop() et loop() qui sont des redéfinitions de méthodes Java qui s’appliquent sur les AudioClip. Ceci permet de simplifier grandement l’utilisation des sons lors de la création des jeux, ainsi : – play() remet le son au début puis appele la méthode start() de Java qui lance le son – pause() appele juste la méthode stop() de Java – stop() coupe le son et le remet au début – loop() permet de faire boucler un son indefinimment (utilisé pour la musique de fond) Ensuite nous avons implémenté une méthode de chargement des sons identique à celle du chargement des images, chaque entity possédant une HashMap soundList, de mode et de son. Au jar du jeu doit etre associé un dossier Sound qui respecte une certaine hiérarchie : – à chaque classe devant jouer un son est associé un dossier portant le nom de la classe – un son à pour nom le mode qui lui est associé : – ainsi pour une classe CbBrick qui joue un son d’explosion lors de sa destruction on aura : – pour hierarchie : Sound/CbBrick/explode.wav et – la méthode CbBrick.getSoundList.get(’explode’).play() pour jouer le son 9 2.2.3 La gestion de la souris De part la nature des jeux créés ou modifiés, la gestion du seul controleur clavier n’était pas satisfaisant pour plusieurs raisons interdépendantes : – un manque de précision du déplacement – une vitesse de déplacement fixe – une utilisation fastidieuse Suite à ce constat, un première implémentation de la souris a été réalisée dans le framework en utilisant l’interface mouseMotionListener pour retranscrire les mouvement de la souris sur l’objet controlé. Premier bilan, l’implémentation a été rapide et le résultat acceptable cependant le curseur de la souris restait par dessus l’objet controlé, ce qui est peu ergonomique. Afin de contourner ce léger soucis, nous avons remplacé le curseur de la zone de jeu par une image vide. Pour laisser la liberté d’utiliser ou non un curseur invisible aux futurs utilisateurs de ce framework, deux modifications du framework ont été nécessaires : – un attribut ’GameZonePanel displayPanel’ a été rajouté dans le gameEngine afin que le gameEngine connaisse la zone de jeu et puisse en modifier le curseur. – une méthode ’void setCursorVisible(boolean visible)’ dans le gameEngine afin que les futurs créateurs de jeu aient un accès facilité à cette fonctionnalité. 2.2.4 L’éditeur de niveau générique Nous avons développé une autre fonctionnalité intéressante et inédite du framework : un éditeur de niveau. Il permet de construire facilement et intuitivement un niveau pour un jeu chargé avec les entités disponibles. Un bouton ’Editor’ a été ajouté dans la fenêtre principale qui permet de lancer l’éditeur après avoir charger un jeu. Si aucun un jeu n’est chargé, il se produit une erreur, sinon l’éditeur se lance et la zone de jeu en cours est effacée. L’éditeur apparait dans une nouvelle fenêtre à côté de la fenêtre principale. Il contient une JList, dans laquelle apparait la liste des entités disponibles pour le jeu chargé, ainsi qu’un JPanel au dessus de la liste qui permet d’avoir un aperçu de l’entité choisie. Quand un jeu est chargé le GameEngine appelle sa méthode loadGamePackage() qui remplit le gamePackage qu’elle a pour attribut. La classe GamePackage contient en fait des listes de toutes les classes d’entité propre au jeu chargé. Le gamePackage est créée grâce à la classe JarLoader et à sa méthode loadJar() qui retourne un GamePackage. Cette méthode ouvre le .jar du jeu à partir du chemin du fichier. Elle parcours ensuite les dossiers et lorsqu’elle rencontre une classe de type Entity (MovableEntity, ControllableEntity, StaticEntity ou BonusEntity) elle ajoute l’objet de type Class dans la liste correspondante. A la fin de la méthode un GamePackage est créé à partir des listes remplies et toutes les classes rencontrées sont chargées donc connues et instanciables par l’application principale. Les Class des 10 Entity ’Animated’ sont placées dans les listes de leur classe mère. Par rapport à l’ancien framework, une nouvelle liste de Class est ajoutée, c’est une liste de classe EntityState qui permet d’utiliser facilement le pattern state sur les Entity, afin notamment de gérer les ’modes’ pour les animations. (pour l’intégration du pattern state : cf 3.2 L’utilisation du Pattern State) La liste des classes contenues dans l’éditeur est construite grâce au gamePackage chargé dans le GameEngine. Toutes les classes ne sont pas ajoutées : les classes héritant de EntityState abstraites (dont le nom fini par ” State ”), ainsi que les classes parmi les Entity ’standard’ qui possèdent une implémentation state ne le sont pas. Pour les reconnaı̂tre il suffit de parcourir la liste des Class de type EntityState et si l’on trouve une classe dont le nom a pour préfixe le nom de la classe que l’on cherche on ne l’ajoute pas. L’instanciation de ces classes se fera à partir de celle de leur état. La sélection d’un élément dans la liste met à jour un attribut de type Class dans le gameEngine ainsi que dans le JPanel qui propose un aperçu de la classe. Dans le gameZonePanel nous avons ajouté un MouseListener qui permet de détecter les évènements souris tels que les clicks. Ainsi quand le bouton gauche de la souris est pressé sur la zone de jeu et si l’attribut instanceToInstanciate du GameEngine n’est pas null, c’est-à-dire si une classe est sélectionnée dans la liste, la méthode instanciate du gameEnigne est appelée. Les coordonnées du click sont passées en paramètre à cette méthode. Une instance du type de classToInstanciate est alors créée et placée dans la liste correspondant à son type dans le gameEngine. Les coordonnées de l’entité sont celles du click, la zone du jeu est redessinée et l’entité est affichée directement. L’instanciation de la classe se fait grâce à la méthode newInstance(). Une instanciation typique s’écrit : classToInstanciate.getConstructor(param).newInstance(new Object [] this). classToInstanciate est le type de l’objet à instancier. param est un tableau de Class qui contient le type des paramètres du constructeur, ici GameEngine. this correspond à l’instance passée en paramètre de type param, ici on utilise this car c’estle gameEngine qui doit être passé en paramètre. Le cas de l’instanciation d’un EntityState est différent puisque il doit aboutir sur la création d’une Entity dans le bon état. On commence donc par créer une instance de classToInstanciate. On récupère ensuite le nom de la classe Entity correspondante au state, ce qui est possible grâce à la structure des noms de classes d’état : <Nom classe Entity>+’State’+<Nom etat> Il faut impérativement respecter cette structure pour que le chargement fonctionne. Grâce à elle l’extraction du nom de la classe est aisé. Une fois le nom récupéré, on récupère l’objet Class par l’appel de la méthode Class.forName (<Nom classe>). Ensuite on créé une instance de cette classe sur laquelle on appelle la méthode setState(EntityState), le paramètre étant l’objet précédemment créé. On arrive ainsi au résultat recherché : un objet Entity dans l’état demandé. Nous avons mis dans la classe d’aperçu un attribut de type Entity que l’on instancie avec l’attribut Class que l’on a récupéré grâce à la liste. Pour cela on utilise la même méthode que précédemment. On affiche ensuite tout simplement l’entité dans le JPanel en faisant attention d’adapter la taille de l’image. Quand l’utilisateur du jeu estime avoir fini la création du niveau, il lui suffit de cliquer sur ’Start Game’ pour commencer la partie instantanément puisque 11 toutes les instances sont chargées dans le moteur de jeu. Nous avons réussi à créer un éditeur de niveau générique qui fonctionne avec tous les jeux créés à partir du framework. Il suffit pour cela que les noms de classes respectent la structure précisée, l’utilisateur du framework n’a pas besoin d’écrire une seule ligne de code pour utiliser cet éditeur. Nous n’avons malheureusement pas pu intégré de système de sauvegarde et de chargement des niveaux faute de temps. 12 2.2.5 Diagramme de classe final simplifié Fig. 2.3 – cf. http ://groups.google.fr/group/terbbfw pour les diagrammes complets 13 Chapitre 3 La création d’un jeu 3.1 Les classes à créer Afin de créer un jeu à partir du framework il est nécessaire de suivre une structure définie ainsi que créer l’ensemble des classes nécessaires. Il faut tout d’abord créer deux packages : view et model, dans lesquels s’inséreront nos diverses classes, ce sont ces dossiers qui seront inclus dans le jar. Comme nous l’avons vu précédemment il faudra aussi créer les dossiers Sprites et Sound pour integrer les sons et les animations. En ce qui concerne les classes, il sera nécessaire de les nommer selon une certaine nomenclature : [Nom du package ][Nom de la classe], par exemple pour une classe Brick dans le package cb.model, la classe aura pour nom CbBrick. Les GEntity associés auront pour nom G[Nom de la classe associée], dans notre exemple cela donnera la classe GCbBrick. Enfin il sera nécessaire de créer les différentes classes du jeu : – dans le model : – le controller (extends Controllable) – les diverses entités (extends Bonus, Movable, Static) – le gestionnaire de jeu (extends GameManager) – dans le view : – les diverses GEntity associées aux Entity précedentes 3.2 L’utilisation du pattern state Lors de la création des Entity il est fortement conseillé d’utiliser un pattern state. En effet ceci permet d’associer divers états à une Entity, ainsi si l’on prend l’exemple du casse-brique, la classe CBBrick on a 7 états différents : – chaque brick a déjà deux états pour son animation, BrickStandard et BrickActive. – des etats qui évolue lors des collisions : – on commence à Red, la brique est rouge – puis lors d’une collision on passe à Purple – ensuite la brique devient bleu (StateBlue) – enfin la brique explose et disparait (StateExplode) 14 Ce design pattern permet ainsi d’éviter de nombreux tests sur les modes pour les animations des entités dans la classe. L’utilisation des sons est elle aussi simplifiée, les sons sont joués lors d’un changement d’état par exemple. Enfin ceci permet d’avoir un code plus propre et aussi de faciliter grandement la création d’un jeu. 3.3 Schéma de classe du casse-brique Fig. 3.1 – cf. http ://groups.google.fr/group/terbbfw pour les diagrammes complets 15 Chapitre 4 Conlusion Bien que nous ne soyons pas parvenu à intégrer tout ce que nous avons noté dans le planning prévisionnel, nous pensons être parvenu à rajouter les fonctionnalités qui nous apparaissait les plus importantes pour rendre le framework le plus complet possible. Il est vrai aussi que nous avons eu quelques difficultés à comprendre certaines parties du code (jarLoader ou gamePackage par exemple), ce qui nous a fait perdre un temps précieux. Cependant la compréhension de ces classes nous a permis d’intégrer plus facilement l’éditeur générique, en réutilisant le systeme de chargement et d’instantiation des classes du GamePackage. En conclusion, on peut dire que bien que nous ayons rencontré certaines difficultés durant l’amélioration du framework, nous avons pu, grâce à ce projet, pousser plus loin nos connaissances en Java, voir les principes fondamentaux de la réutilisabilité ainsi que comprendre clairement le fonctionnement du pattern state, qui s’avère très utile. 16