Etape 9 – Désérialisation de l'état

1 Introduction

Le but de cette étape est de commencer le développement du client en écrivant les classes permettant de désérialiser l'état sérialisé par le serveur, c-à-d de transformer la séquence d'octets envoyée par le serveur en une représentation sous forme d'objets.

1.1 Etat désérialisé

Le client a une vision très simplifiée, et principalement visuelle, de l'état du jeu. En effet, son seul but est de pouvoir correctement représenter cet état à l'écran, et il n'a donc pas besoin d'en connaître les aspects qui n'affectent pas cette représentation. Dès lors, après désérialisation, l'état du jeu possède les composantes suivantes :

  1. les caractéristiques des quatre joueurs (identité, nombre de vies, position, image),
  2. les images représentant le plateau de jeu,
  3. les images représentant les bombes et explosions,
  4. les images représentant la ligne des scores,
  5. les images représentant la ligne du temps.

Comme on le voit, à l'exception des joueurs, chacune de ces composantes est constituée d'une séquence d'images.

Dans le cas du plateau de jeu et des bombes et explosions, ces images sont celles calculées par les peintres dans le serveur, et le client se contente de faire correspondre les octets reçus aux images qu'ils identifient.

Il en va toutefois autrement de la ligne des scores et de la ligne du temps, affichées en dessous du plateau de jeu (voir figure 1). Celles-ci sont construites dans le client en fonction des données relatives aux joueurs et au temps, et il convient donc de décrire rapidement leur composition.

score-time-lines.png

Figure 1 : Lignes des scores (en bleu) et du temps (en orange)

1.2 Ligne des scores

La ligne des scores est la ligne placée directement en dessous du plateau dans la représentation graphique du jeu. Elle montre une image du visage de chaque joueur, différente si celui-ci est vivant ou mort, ainsi que le nombre de vies qu'il lui reste. Les images permettant de représenter la ligne des scores sont celles du sous-répertoire score de l'archive fournie avec l'étape 7.

La ligne des scores est composée d'un total de 20 images :

  • 12 d'entre elles représentent l'état des joueurs (3 images par joueur),
  • 8 d'entre elles sont du remplissage, placé entre la représentation de l'état du joueur 2 et celle du joueur 3.

L'état de chaque joueur est représenté par une succession de trois images qui, de gauche à droite, sont :

  1. l'image montrant le visage du joueur, de face, soit vivant soit mort,
  2. une image de remplissage qui donne au rectangle correspondant au joueur la taille nécessaire pour qu'il soit possible d'y afficher son nombre de vies,
  3. une image terminant ce rectangle.

Pour le joueur 1, l'image de son visage a l'index 0 lorsqu'il est vivant, 1 lorsqu'il est mort. Pour le joueur 2, ces images ont respectivement les index 2 et 3, et ainsi de suite. L'image à placer juste après le visage de chaque joueur a l'index 10 (fichier 010_text_middle.png) et celle à placer juste après celle-ci a l'index 11 (fichier 011_text_right.png). Finalement, l'image à utiliser pour le remplissage de la partie centrale a l'index 12 (fichier 012_tile_void.png).

Il est important de noter que le nombre de vies de chaque joueur n'est pas représenté par une image. Au lieu de cela, le texte correspondant au nombre de vies de chaque joueur (3 pour tous les joueurs, dans l'exemple ci-dessus) est superposé, ultérieurement, aux images de la ligne des scores.

1.3 Ligne du temps

La ligne du temps est la ligne placée tout en bas de la représentation graphique du jeu, et elle montre le temps restant par unités de 2 secondes au moyen de petits rectangles pleins ou vides. Les images permettant de la représenter sont également dans le répertoire score de l'archive fournie avec l'étape 7. Il n'y en a que deux :

  • celle d'index 20 (fichier 020_led_off.png) contient l'image d'un rectangle vide,
  • celle d'index 21 (fichier 021_led_on.png) contient l'image d'un rectangle plein.

La ligne de temps est composée de 60 images, les n premières représentant un rectangle plein, les autres un rectangle vide, où n est le temps restant en unités de 2 secondes.

2 Mise en œuvre Java

La totalité des classes à écrire dans le cadre de cette étape sont à usage exclusif du client. Dès lors, elles sont placées dans le (nouveau) paquetage ch.epfl.xblast.client, qui lui est réservé.

2.1 Classe ImageCollection

La classe ImageCollection, publique, finale et immuable, représente une collection d'images provenant d'un répertoire et indexées par un entier. Son but est de permettre d'obtenir les images des différents éléments du jeu en fonction de l'entier (octet) qui leur correspond.

A chacun des quatre sous-répertoires d'images fournies à l'étape 7 (block, explosion, player et score) correspond une telle collection d'images. Par exemple, le sous-répertoire contenant les images des bombes et explosions, nommé explosion, contient un total de 18 images, chacune identifiée par un entier représenté en base dix dans les trois premiers caractères de son nom de fichier. Les images contenues dans ce sous-répertoire sont représentées par une instance de ImageCollection, qui donne accès à une image particulière en fonction de son index. Par exemple, l'index 20 donne accès à l'image de la bombe, étant donné qu'elle est contenue dans le fichier nommé 020_bomb.png.

La classe ImageCollection offre un unique constructeur prenant en argument le nom d'un répertoire contenant des images, par exemple player ou explosion.

En plus de son constructeur, la classe ImageCollection offre deux méthodes permettant d'obtenir une image d'index donné, sous la forme d'une instance d'une sous-classe de Image. La seule différence entre ces deux méthodes, nommées p.ex. image et imageOrNull, est leur comportement dans le cas où aucune image ne correspond à l'index dans la collection :

  • la première lève l'exception NoSuchElementException,
  • la seconde retourne null.

La première est à utiliser lorsqu'on a la certitude que l'index est valide, la seconde lorsque ce n'est pas le cas. Dans la version actuelle du jeu, seuls les index représentant les images des joueurs ou des bombes et explosions peuvent être invalides, étant donné qu'un index invalide est utilisé pour représenter l'absence d'image.

Au moyen de la classe ImageCollection, et en fonction de ce qui a été dit plus haut, il devrait être possible d'obtenir l'image de la bombe au moyen de l'extrait de programme suivant :

ImageCollection c = new ImageCollection("explosion");
Image bomb = c.image(20);

2.1.1 Obtention du répertoire

Pour pouvoir construire une collection d'images, il faut pouvoir accéder au répertoire les contenant. Or cela n'est pas totalement trivial, sachant que ce répertoire se trouve probablement à un endroit différent sur le disque dur de chaque étudiant.

Pour résoudre ce problème, il convient d'utiliser la notion de resource offerte par Java. Une resource est simplement un fichier ou un répertoire qui est stocké au même endroit que les fichiers compilés du projet (.class). Dans le cas d'Eclipse, cela signifie dans le sous-répertoire bin du projet. Il est possible d'avoir accès à ces resources en connaissant uniquement leur chemin relatif et pas leur chemin absolu, au moyen de différentes méthodes.

Dans le cas de la classe ImageCollection, le code ci-dessous — dont la compréhension détaillée n'est pas nécessaire — permet d'obtenir, sous la forme d'une instance de la classe File, le sous-répertoire contenant un groupe d'images en fonction du nom de celui-ci, p.ex. player ou score :

String dirName = …;             // p.ex. "player"
File dir = new File(ImageCollection.class
                    .getClassLoader()
                    .getResource(dirName)
                    .toURI());

Si vous constatez une erreur lors de l'exécution de ce code, vérifiez que le répertoire contenant les images du projet (nommé images) est bien un répertoire source. Cela peut se voir dans l'explorateur de paquetages (Package Explorer), où l'icône lui correspondant doit avoir le même aspect que l'icône du répertoire src. Si tel n'est pas le cas, faites un clic droit sur le répertoire images, puis sélectionnez Build Path et Use as Source Folder.

Attention : utilisez impérativement le code ci-dessus pour obtenir le répertoire correspondant à une collection d'images dans votre projet. Si vous modifiez ce code, il est possible qu'il fonctionne sur votre ordinateur mais pas sur celui qui sera utilisé lors de la correction, ce qui aurait la fâcheuse conséquence de rendre votre projet impossible à lancer, vous faisant du même coup perdre la totalité des points attribués au test du projet terminé. Si le code ci-dessus ne fonctionne pas chez vous, demandez de l'aide, mais ne le modifiez sous aucun prétexte.

2.1.2 Chargement des images

Une fois le répertoire obtenu sous la forme d'une instance de File, il convient d'en charger la totalité des images. Dans ce but, il faut :

  1. itérer sur les fichiers qu'il contient, qui s'obtiennent au moyen de la méthode listFiles,
  2. pour chaque fichier du répertoire, essayer de convertir les trois premiers caractères de son nom (obtenu grâce à la méthode getName) en un entier, au moyen de la méthode parseInt de Integer,
  3. lire le fichier pour obtenir l'image correspondante au moyen de la méthode read de ImageIO.

La plupart de ces opérations peuvent produire une erreur (sous la forme d'une exception), auquel cas le fichier incriminé doit simplement être ignoré, sans que cela n'ait d'influence sur le reste du processus.

2.2 Classe GameState

La classe GameState, publique, finale et immuable, représente l'état du jeu du point de vue du client. Pour mémoire, cet état est un tuple composé des cinq valeurs suivantes :

  1. les quatre joueurs,
  2. les images représentant le plateau de jeu,
  3. les images représentant les bombes et explosions,
  4. les images représentant la ligne des scores,
  5. les images représentant la ligne du temps.

Les joueurs sont stockés dans l'ordre de leur identité dans une liste, chacun étant représenté par une instance de la classe (imbriquée) Player décrite ci-dessous.

Les images sont quant à elles représentées par des valeurs de type Image stockées dans une liste, toujours dans l'ordre de lecture, c-à-d de haut en bas et de gauche à droite à l'écran. Cela est valable même pour la liste des images du plateau de jeu, quand bien même celle-ci est sérialisée dans l'ordre en spirale.

2.3 Classe GameState.Player

La classe Player, publique, finale, immuable et imbriquée statiquement dans la classe GameState, représente un joueur du point de vue du client. Là aussi, ce point de vue est très différent et nettement moins sophistiqué que celui du serveur (et de sa propre classe Player). Ainsi, du point de vue du client, un joueur est composé de :

  1. son identité (de type PlayerID),
  2. son nombre de vies (de type int),
  3. sa position (de type SubCell),
  4. son image (de type Image).

2.4 Classe GameStateDeserializer

La classe GameStateDeserializer, publique, finale et non instanciable, représente un désérialiseur d'état. Elle constitue, en quelque sorte, l'inverse de la classe GameStateSerializer écrite lors de l'étape précédente.

Elle offre une unique méthode publique (et statique), nommée p.ex. deserializeGameState qui, étant donné une liste d'octets représentant un état sérialisé, de type List<Byte>, retourne une représentation de l'état du jeu, de type GameState. Cette méthode étant plutôt longue, il est fortement conseillé de la découper en plusieurs méthodes privées et chargées chacune de la désérialisation d'une partie de l'état (plateau, bombes et explosions, etc.). Chacune de ces méthodes privée peut recevoir la liste des octets qui la concerne, obtenue au moyen de la méthode subList appliquée à la liste principale.

A noter que la « désérialisation » de la ligne des scores et de la ligne de temps implique de les générer en fonction d'autres parties de l'état désérialisé, selon les indications données plus haut.

Lors de la désérialisation, il est crucial de correctement distinguer les octets signés et non signés, comme cela a été expliqué dans l'énoncé de l'étape précédente. Les octets signés ne requièrent pas de traitement particulier, étant donné que les valeurs de type byte sont toujours interprétée comme des valeurs signées en complément à deux en Java. Par contre, les octets non signés doivent être traités spécialement. La manière la plus simple de les interpréter consiste à utiliser la méthode toUnsignedInt de la classe Byte qui, étant donné un octet (de type byte) retourne l'entier (de type int) correspondant aux bits de cet octet, interprétés de manière non signée.

2.5 Tests

A ce stade il est relativement difficile d'effectuer des tests de l'embryon de client écrit dans cette étape. Il vaut néanmoins la peine de vérifier que le désérialiseur est capable de désérialiser sans erreur tous les états d'une partie aléatoire, par exemple.

L'étape suivante sera consacrée à la représentation graphique de l'état à l'écran, et une fois qu'elle sera terminée il deviendra plus simple de constater visuellement d'éventuels problèmes.

3 Résumé

Pour cette étape, vous devez :

  • écrire les classes ImageCollection, GameState, GameState.Player et GameStateDeserializer en fonction des spécifications données plus haut,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies.