rapport inachevé

Transcription

rapport inachevé
Mathieu BAUEMLER
David HOEUNG
SOMMAIRE
I) PRÉSENTATION DU JEU
I.1) Espace de jeu
I.2) Règles
II) OBJECTIFS
III) CONCEPTION - ARCHITECTURE DES CLASSES
IV) MOTEUR D’AFFICHAGE
IV.1) Multiple-buffering
IV.2) display.Screen
IV.3 ) Les sprites
IV.4) Transformations
IV.5) De l’avantage, et du désavantage, du double buffering.
V) REPRÉSENTATION DES DONNÉES
V.1) Problèmes posés
V.2) Solution apportée:
V.3) Diagramme de classe
V.4) Algorithmes (and blues)
V.5) Explosion
V.6) Chute
V.7) Conclusion
VI) GESTION DES JOUEURS (RESEAU ET CLAVIER)
VI.1) Protocole réseau
VI.2) Les données qui transitent
VI.3) Quand ?
VII) DEROULEMENT DU JEU
VIII) GESTION DE REGLES DU JEU
IX) PRÉSENTATION
IX.1) Chargement
IX.2) Menu
I) PRÉSENTATION DU JEU
Puzzle Bobble est un petit jeu d’arcade de chez Taito, édité en 1994.
Bub et Bob, les deux petits dragons, héros du célèbre jeu Bubble Bobble, (également de chez
Taito, sorti en1986 sur une grande variété de systèmes, dont la Sega Master System et la Nintendo Entertainment System), sont de retour dans Puzzle Bobble.
En tant que joueur, on s’identifie à l’un ou à l’autre de ces deux personnages, ce qui est très
important pour l’immersion dans le jeu.
Ce genre de jeu, joué seul, est aussi passionnant que de faire ses lacets. Par contre, à
deux, tout l’intérêt du jeu se revèle, car il mêle à la fois vitesse, réflexion, stratégie et adresse,
ce qu’on retrouve dans les Tetris-comme.
I.1) Espace de jeu
Chaque joueur possède sa propre zone de jeu. Cette zone est destinée à contenir des
boules colorées. Les boules sont envoyées dans cette zone une à une, par le joueur, à l’aide
d’une espèce d’arbalète. Il peut diriger le viseur de l’arbalète et ensuite tirer dans la zone bidimensionnelle du jeu, pour atteindre la destination la plus stratégique. Il dispose d’un temps
limité pour chaque tir. Au delà de ce temps, la boule est tirée. Dès qu’une boule est tirée, une
autre est prête à être tirée dès que la boule arrive. La couleur de la boule est choisi par le jeu.
I.2) Règles
Les règles sont très simples. Lorsque l’écran de jeu d’un joueur est pleine de boules,
et surtout qu’une ou plusieurs boules dépassent la limite inférieur de la zone de jeu, le joueur
perd. Que peut-on bien faire pour empêcher cela ?
Quand plus de deux boules de même couleur sont adjacentes suite à l’envoi d’une boule, ces
boules adjacentes explosent.
Si des boules étaient fixées à la zone de jeu uniquement par les boules explosées elles tombent jusqu’à disparaître de la zone de jeu. Après un savant calcul, suivant le nombre de boules tombées, et le nombre de boules explosées, un certain nombre de boules sont ajoutés dans
la zone de jeu de l’adversaire, pour le perturber.
La méthode magique observée pour calculer le nombre de boules :
- compter le nombre de boules explosées. si ce nombre dépasse 3, sa différence avec 3 est
ajoutée au nombre de boules envahissantes.
- compter le nombre de boules tombantes. au cours d’une partie, pour un joueur, une fois sur
deux
1) pour une boule tombante, on ajoute un au nombre de boules envahissantes
2) pour une autre boule tombante, on ajoute deux boules supplémentaires à l’adversaire.
Donc plus le nombre de boules explosées est important et surtout plus le nombre de boules
tombantes est élevé, plus l’adversaire recevra de boules envahissantes.
Ces boules envahissantes sont placées dans une file d’attente. Dès que le malheureux joueur
tire, si des méchantes boules étaient en attente, elles arrivent dans la zone de jeu par maximum de sept.
Pour compliquer encore un petit peu les choses, régulièrement (à chaque fois qu’un joueur
tire seize boules), une ligne de boules est ajoutée en haut de la zone de jeu, et fait descendre
toutes les autres.
II) OBJECTIFS
Notre objectif est de restituer fidèlement le jeu (le gameplay).
L’attention sera beaucoup moins portée, sur le côté esthétisme. Nous reprendront simplement
des éléments du décor, les boules, la flèche, du jeu original, à partir d’un émulateur de consoles de jeu.
Il sera réalisé en Java, et uniquement jouable par deux joueurs, un par machine. Un des
joueurs exécutera un serveur, l’autre un client.
Au niveau du jeu, tout sera respecté :
- une boule qui doit tomber, tombera.
- une boule qui doit exploser, explosera ou disparaîtra.
- des boules pourront être envoyés chez l’ennemi.
- elles seront également dans une file d’attente visible.
- le joueur aura une dizaine de secondes pour tirer.
- le temps restant sera indiqué pour les 5 dernières secondes.
- les joueurs auront des boules de couleur identiques. (Nous n’avons pas déterminé, quel était
l’algorithme qui définissait la couleur de la prochaine boule, car il semble y en avoir un).
- le joueur pourra voir la situation de son adversaire. (Sur les deux programmes, il y aura, les
deux écrans des deux joueurs. Nous avons décidé, que le joueur local, sera le joueur de gauche, et le joueur distant, sera celui de droite.
III) CONCEPTION - ARCHITECTURE DES CLASSES
Nous avons essayer autant que possible de séparer la représentation des données et
l’affichage, sans toutefois mettre en place le pattern Model - View - Controller.
Dans le cas d’un jeu, nous pensons qu’il n’est pas nécessaire de mettre en place un
vrai MVC. La vue est constamment mise à jour. Il n’est pas vraiment nécessaire de mettre en
place un système de notification de changement du modèle vers la vue lorsqu’un controlleur
modifie le modèle, puisque dans tous les cas, la vue est mise à jour régulièrement.
De plus nous n’avons qu’un seul acteur qui modifiera ce modèle, c’est le joueur, par
l’intermédiaire des touches du clavier. Le joueur réseau, sera également un contrôleur, mais
aura son modèle correspondant. Il n’agit pas directement sur le modèle de l’adversaire.
Notre contrôleur, sait très bien à quel instant le modèle a été modifié, de même qu’il
sait ce qui doit être mis à jour dans la vue.
Nous allons donc simplement créer des classes qui s’occupent de la gestion du modèle
(dans un package model), et d’autres de la gestion de la vue (dans un package display).
Nous pouvons créer une classe Game ce qui donnerait le moyen de créer plusieurs parties (différents joueurs) ou manches, en instantiant un nouvel objet Game.
L’architecture général ressemble donc à :
Un objet Game, possède son joueur local (KeybPlayer), et possède son joueur distant (DistantPlayer). Chaque joueur interagit sur son modèle (BubbleArray) et sur sa vue (Screen).
KeybPlayer et DistantPlayer hérite bien sûr de toutes propriétés de Player.
IV) MOTEUR D’AFFICHAGE
Avant de commencer à gérer l’affichage pour Puzzle Bobble, connaissant assez mal les
capacités d’animation de Java, nous avons procéder à une série de test avec des sprites variés
se déplaçant anarchiquement dans une fenêtre. Nous avons étudier des exemples en plein
écran, mais nous avons préférer utiliser un mode fenêtré.
Notre série de tests nous a convaincu, même si c’est assez évident, que tous les sprites
ne sont pas à réafficher. Redessiner ceux qui sont restés immobiles sera plus coûteux pour la
machine que de réafficher uniquement les sprites qui se déplacent.
Nous avons donc été confronté à deux problèmes :
- si on ne réaffiche pas tout, les parties non redessinées risque d’être effacées lorsqu’une fenêtre passe en avant plan sur ces parties.
- comment limiter la zone à redessiner.
IV.1) Multiple-buffering
Le premier problème est très simplement résolu en utilisant un double-buffering.
Un buffer contient toute la zone d’affichage. On dessine dans ce buffer, lorsque l’on souhaite réafficher, on ‘blitte’ le buffer : il est copié dans le buffer d’affichage, et donc afficher.
D’après nos recherches, cette opération est plus efficace que d’utiliser la méthode Paint() des
objets java.awt.Component, puisqu’on dessine dans un buffer, et lorsqu’on a terminé, on copie vers l’affichage une seule fois, grâce au travail effectué dans le buffer. De plus la méthode
Paint() est gourmande car elle rafraîchit le contexte de fenêtrage.
Nous faisons appel au BufferStrategy, car Java améliore encore les performances en gèrant
lui même les buffer à utiliser et ou à ‘flipper’ (page flipping). L’efficacité obtenue avec l’emploi du BufferStrategy, a été remarquée lors de nos tests : nos animations ne clignotaient
plus.
IV.2) display.Screen
Le meilleur moyen de savoir, quel zone doit être redessinée, est de gérer les différents
sprites, afin de pouvoir minimiser les parties à mettre à jour. En effet il peut y avoir différentes zones à redessiner, si le joueur tourne son viseur, pendant qu’une boule est tirée ou que
d’autres boules tombent de manières éparses.
IV.3 ) Les sprites
Nous avons créé une classe Sprite afin de gérer les problèmes de recouvrement des
sprites, ainsi que les différentes transformations que nous allons leur faire subir. Dans un
premier temps, nous avions élaborer un algorithme qui ne redessinait que les sprites qui
avaient subit un changement, ainsi que ceux qui les recouvraient partiellement ou qui étaient
recouverts. Mais un minimum d’observation nous à montré que la disposition globalement
héxagonale des boules les faisaient toutes se recouvrir en partie. Donc une boule arrivant au
voisinage de celles placées provoquait une réaction en cascade les faisant toutes se redessiner.
Nous avons donc décider d’utiliser le clipping. Chaque Sprite possède donc une Area,
qui délimite ses contours lors d’une transformation. Puis on calcule l’intersection de chaque
sprite avec cette Area, et on ne redessine que cette portion des sprites. On remplace donc
l’affichage de tous les sprites par un calcul sur des rectangles.
les frontières des sprites dessinées.
IV.4) Transformations
Nous avons mis en place différentes transformations suivant nos besoins:
* TranslatingSprite / Bubble : Ces sprites sont capables de se déplacer sur tout l’écran
suivant une translation par rapport à l’origine. Ce sont les sprites qui demandent le moins de
temps de calcul sur leur transformation, mais comme ce sont les plus nombreux, ils deviennent finalement aussi gourmands que les autres.
* RotatingSprite : Créé pour le viseur. Appliquer une rotation sur un sprite se révèle
très couteux, tant sur la transformation que sur le calcul des intersections. Heureusement, il
n’y a que deux flèches par écrans.
* ScalingSprite : Créé pour le compte à rebours du tir automatique. Eux aussi sont très
gourmands en termes de calcul, mais eux aussi sont limités à deux par écran. Ces sprites peuvent se déplacer tout comme les TranslatingSprite, mais ils peuvent aussi changer de taille.
IV.5) De l’avantage, et du désavantage, du double buffering.
Nous avons donc implanté une stratégie de double buffer, afin d’accélerer l’affichage,
déjà ralentit par les calculs sur les RotatingSprite et les ScallingSprite. Certes, les avantages
du double buffering sont évident, surtout s’ils sont accompagnés de l’utilisation d’images
volatiles, c’est à dire d’images disposant de l’accélération graphique présente sur la machine. Malheureusement, la contre-partie à cette stratégie se trouve être....le double buffer
lui-même, lorsque cette fonction n’est pas supporté par le système. Les machines dont nous
disposont à l’université nous l’ont prouvé. En effet, nous faisons un test de compatibilité au
début de l’application, et si le multibuffer n’est pas disponible, l’application s’arrète. C’est
ce qui se passe dans nos salles, sur les postes Unix. Si nous enlevons ce test, les performances chutent de façon dramatique puisque Unix cherche à réaliser le double buffer en soft....
En dehors de cela, sous Windows (merci aux très bonnes API graphiques disponibles sous ce
système), les performances sont plus que correctes.
V) REPRÉSENTATION DES DONNÉES
V.1) Problèmes posés
A la différence de beaucoup de jeux de l’époque, on distingue de suite que le tableau
qui gère les boules n’est pas du type, fort agréable, rectangulaire de base. Hé non, nous avons
affaire à un pavé hexagonale.
Bien sûr, on pourrait se dire qu’il suffit de le stocker dans un simple tableau à deux
dimensions et de jouer intelligement sur les indices. Donnons un exemple qui complique
(contredit) cette idée.
Prenons le cas d’une ligne ayant 8 boules, et une ligne juste en dessous en ayant donc
7. Pour la première boule de la premiere ligne, son unique voisin de la ligne
suivante a pour indice 0. Pour la première boule de la deuxieme ligne, ses voisins de la ligne
précedente ont pour indice 0 et 1.
Damned, ce n’est pas très générique...
V.2) Solution apportée:
Nous avons donc découpés notre BubbleArray en lignes de différentes tailles, chacune
contenant des cases connaissant tous ses voisins. Notre ‘tableau’ est aussi circulaire, ce qui
signifie que la derniere ligne a pour voisin suivant.... la première ligne. Cela nous permet
très facilement de rajouter la fameuse ligne des 16 tirs, en permuttant les indices. De plus, la
création des listes de voisins pour chaque case s’en retrouvent simplifiée, puisque la première
et la dernière ligne ne sont pas différentes des autres.
note: la seule précaution à prendre est d’instancier le tableau avec une ligne de plus que le
jeu n’en comporte, afin d’éviter les effets de bord lors de la recherche des boules qui explosent. (sinon, jeter une boule trop bas, dans la dernière ligne, pourrait conduire à supprimer
des boules de la première, puisqu’elles sont voisines !).
V.3) Diagramme de classe
note: Une autre incartade au pattern MVC est commise ici. Chaque case du tableau contient
en effet un lien vers le sprite qu’elle contient, si elle en a un. La raison en est expliquée dans
la conclusion.
V.4) Algorithmes (and blues)
Les algorithmes utilisés pour détecter les boules qui doivent disparaître et celles qui
doivent tomber nous ont été inspirés par ceux utilisés en traitement de l’image (reconstruction géodésique):
V.5) Explosion
On commence par initialiser une pile et une liste avec la boule qui vient d’être lancée.
Puis, on déroule l’algorithme suivant : tant que la pile est non-vide, on prend la première
boule de la pile. On insère dans la pile et dans la liste toutes les voisines identiques et non
‘taggées’, que l’on marque donc d’un tag unique. Si la taille de la liste est supérieur à 2, alors
on doit faire exploser les boules qui y sont contenues et il faut rechercher celles qui peuvent
tomber.
V.6) Chute
On initialise une pile avec les boules de la première ligne. Puis ,tant que la pile est non
vide, on prend la première boule. On marque toutes ses voisines non marquées d’un tag unique, et on les place dans la pile. On les rajoute aussi dans la liste construite plus haut. A la fin
de l’algorithme, toutes les boules non marquées doivent tomber.
V.7) Conclusion
Afin de faciliter le travail de la partie affichage, chaque boule possède un champ State,
qui décrit en permanance son état (prêt à partir, en route, en place, en train d’exploser, en
train de chuter). Ces deux derniers champs sont mis à jour dans les algorithmes précédemment décrits, et la liste des boules qui explosent/tombent est transmise à la partie Display.
C’est là tout l’intérèt d’enfreindre le MVC pour ne pas avoir à refaire une correspondance
entre les coordonnées des cases et celles des sprites.
VI) GESTION DES JOUEURS (RESEAU ET CLAVIER)
Plusieurs questions se sont posées immédiatement :
- Quel protocole faut-il utiliser ?
- Quelles données doivent-êrte envoyées / reçues ?
- Quand faut-il transmettre ces informations ?
VI.1) Protocole réseau
Nous avons eu deux possibilités principales :
Utiliser un mode non connecté (UDP) ou un mode connecté (TCP).
Le mode non connecté ne nous garantit pas l’arrivée et le séquençage correct de nos informations. Pour utiliser ce mode, il nous faudrait établir un protocole qui nous permette de
maintenir la synchronisation entre nos deux applications, de retransmettre les informations
égarées, si le protocole l’exige.
Envoyer l’état complet du jeu, par exemple, éviterait les demandes de retransmission, car les
deux programmes peuvent se resynchroniser.
Est-ce que l’utilisation d’un tel mode, minimiserait le nombre d’envoi de messages ?
L’observation de la plupart des joueurs de Puzzle Bobble, montre que le viseur est constamment en train de tourner. Il faut donc envoyer très régulièrement des messages, pour que le
jeu soit synchronisé.
Nous avons donc décidé d’utiliser un mode connecté, c’est à dire TCP, car l’envoi de messages risque d’être fréquent, et nous évitera de vérifier que tous les paquets ont bien été reçus.
VI.2) Les données qui transitent
Il est, à notre avis, préférable de restreindre la quantité de données sur le réseau. Le
réseau n’a pas vraiment besoin d’être encombré par Puzzle Bobble.
La raison n’est pas vraiment valable. La question est de savoir s’il vaut mieux
- ne pas envoyer les informations que l’on peut déduire de l’autre côté
- envoyer des informations pour éviter que le distant n’ait à les déduire.
Nous avons choisi d’envoyer le minimum, ce qui allége surement la charge réseau, l’inconvénient est que les deux postes effectuent le même travail. (servlet ou applet ?), mais bon le
résultat doit être meilleur. Les informations que nous envoyons serons simplement les touches sur lesquels le joueur distant appuie. (très certainement le minimum). L’inconvénient
est qu’il est impératif que nos deux programmes soit parfaitement synchronisé, puisque nous
n’effectuons pas resynchronisation de l’état du jeu.
VI.3) Quand ?
Deux solutions :
- envoyer lors d’un changement d’état / événement
- envoyer constamment
VI.3.1) Quand il faut ...
Envoyer uniquement lorsqu’un événement se produit. Il faut donc mettre en place un
ou plusieurs java.langThread qui écouteront et/ou enverront les messages. Nous avons eu à ce
niveau une crainte : celle d’être désynchronisé, puisque nous envoyons le minimum.
Par exemple d’un côté le joueur a tiré. Le 2ème joueur a fait tomber des boules, donc le 1er
va le recevoir. Il y a un risque pour que
- d’un côté le 1er joueur ait tiré et reçoit des boules dans sa file d’attente.
- de l’autre côté le 1er joueur ait tiré après avoir reçu les boules de l’adversaire.
Ces deux situations sont totalement différentes au niveau du jeu.
Comment être sûr dans ce cas là que l’état du jeu est exactement le même des deux
côtés. Une solution aurait été d’envoyer régulièrement l’état du jeu. Est-ce qu’un décalage ou
un ralentissement n’aurait pas été introduit.
Nous n’avons pas eu le temps d’expérimenter cette solution.
VI.3.2) Tout le temps ...
La 1ère (et la seule) méthode que nous essayée est la suivante. Un message est attendu
régulièrement de la part des deux applications. Une des deux applications envoie en premier
son message (touche pressée) puis l’autre envoie son message. Nous avons appelé l’application qui envoie le premier message et l’autre client. C’est la seule différence avec le serveur
et le client. C’est également la différence entre SlavePlayer et MasterPlayer.
Cette solution est très simple à mettre en oeuvre, et nous permet d’être synchroniser à chaque
instant du jeu.
VI.3.3) Précautions ...
Le KeybPlayer est en fait un java.awt.event.KeyListener.Il reçoit l’événement touche
pressée et touche relachée, et mets à jour un tableau d’états de touches pour le jeu. On évite
ainsi l’effet de répétition des touches (event KeyPressed, long à se déclencher lorsque la touche est maintenu) et on obtient une meilleure réactivité.
La précaution qui est prise, vient du fait que le Listener peut se déclencher n’importe
quand, par exemple, juste avant qu’on envoie au programme distant, la touche qu’on utilise
pour modifier l’état du jeu. Résultat, malgré notre solution discutable de synchronisation, le
jeu ne sera plus le même sur les deux machines. Nous avons donc mis cette partie d’envoi de
la touche en section critique.
VII) DEROULEMENT DU JEU
Puisque nous avons une synchronisation à partir du réseau (les envois de messages
étant bloquant), et que ces messages correspondent à des touches, entre deux messages,
client et serveur ont le comportement pour les mêmes touches à gérer. Il s’agit donc entre
deux messages de mettre à jour le modèle et l’affichage si nécessaire.
Extrait de main.Game.playRound()
setReadOnly() et setReadWrite() délimite la section critique.
parle() envoie la touche pressée.
nextMove() fait le nécessaire dans le modèle et la vue pour réagir par rapport à la touche.
render() affiche la vue.
ensuite un temps de pause est marqué, suivant le temps mis depuis le cycle précédent, pour
obtenir une fréquence de jeu régulière.
VIII) GESTION DE REGLES DU JEU
Afin de pouvoir répondre au différentes états du jeu :
- boules en train d’être tirée
- boules qui tombent
- boules qui envahissent
- compte à rebours du temps
- ligne de boules insérée
un package action a été créé, une classe pour gérer chacun de ces états, orchestrés par la méthode gamer.Player.nextMove()
Le rôle de Tireur est gérer la trajectoire de la boule tirée, de transmettre à Insereur, qu’une
boule a été tiré, et d’envoyer à Grappeur le nombre de boules qui tombent, de modifier le
modèle et la vue.
Insereur ajoute une ligne lorsqu’il est temps de le faire.
Grappeur s’occupe de faire tomber les boules.
Trigger déclenche un tir lorsqu’il est trop tard. Il positionne seulement la touche avec la valeur de tir.
Grimpeur s’occupe de faire grimper les boules envahisseuses.
Un Player possède un objet de chacune de ces classes.
La méthode nextMove() est en fait un automate.
Chaque état est gouverné par l’objet correspondant, précédemment cité.
L’ état terminal n’a pas été mentionné, lorsque la partie est perdue, l’objet action.Nettoyeur
s’occupe de griser les boules du perdant.
IX) PRÉSENTATION
Comme on peut s’en rendre compte rapidement, notre application est assez lente en
temps de chargement, tout comme elle est gourmande en mémoire. Nous allons essayé d’en
expliquer les raisons.
IX.1) Chargement
Le chargement se décompose en deux phases:
IX.1.1) Les sons
Nous avons, autant que faire se peut, essayé de recréer l’ambiance régnant dans
le mythique (mitique est le notre) Puzzle Bobble original. Pour cela nous avons inclut des
événements sonores et des musiques. L’inconvénient principal est que nous avons dû gérer
une bibliothèque de sons préchargés en mémoire. L’avancée du chargement est visible dans
la console, en mode texte.
IX.1.2) Les images
Nous préchargeons évidemment toutes les images des sprites à l’avance. L’avancée du chargement est là encore visible dans la console, mais aussi par le biais d’une barre de
progression dessinée sous le logo du jeu. Cette barre n’est pas un fake, si le temps de chargement est aussi long, c’est parce que nous créons à la volée des graphiques additionnels. En
effet, les boules grisées qui remplacent les boules colorées lorsque un joueur perd ne sont pas
des fichiers mais des images créées à partir de celles en couleurs. L’utilisation d’un filtre sur
une BufferedImage reste un processus coûteux. (Mais que nous voulions tester).
IX.1.3) Conclusion
Le chargement des sons et des images conduit malheureusement à un temps
d’attente plus ou moins long selon les machines, mais avouons qu’un peu de musique rend
une partie bien plus intéressante.
IX.2) Menu
Lorsque la phase de chargement est terminée, un menu apparaît portant l’image d’introduction du jeu original, et la musique d’ouverture se fait entendre. Les menus proposés
sont d’une grande simplicité.
* un bouton de selection permet de choisir entre serveur et client (respectivement créer
et rejoindre)
* Si rejoindre est coché, il faut indiquer l’adresse du serveur dans le champ texte.
* Jouer permet de lancer une partie.
* Stop permet d’interrompre une partie en cours.
Le jeu se lance alors dans une petite fenètre :
Les codes graphiques de Puzzle Bobble sont donc biens respectés (dans l’ensemble).

Documents pareils