JSR 353
Avant de commencer une petite précision concernant la JSR 353 : jusqu’à présent, j'avais utilisé l’implémentation de référence de l'API JSON-P (la version 1.0), mais ceux qui utilisent Maven se sont peut-etre rendu compte qu'il en existait une version plus récente. Il est en effet possible de télécharger des mises à jours via le site The Central Repository qui permet de faire des recherches dans le repo central de Maven. Ainsi à l'heure actuelle, la version la plus récente est la version 1.0.4 datant du 19 novembre 2013.
Charabia légal
Guild Wars et Guild Wars 2 sont des marques déposées de ArenaNet et NCsoft. De plus, toutes les images restent la propriété de ArenaNet. De même, la Web API utilisée reste la propriété de ArenaNet qui peut décider de la retirer ou d'en interdire l'utilisation comme bon lui semble. Et bien sûr, je ne suis affilié ni avec ArenaNet, ni avec NCsoft.
Contexte
Dans le cadre du jeu, les teintures jouent un rôle important pour faire varier l'aspect visuel des joueurs. Ainsi malgré un nombre correct de races, de taille de corps, visages, de coiffures, de couleur de pilosité pour les deux sexes (plus encore si on prend en compte les coiffures, visages et couleurs de cheveux exclusifs disponibles dans les kits en vente dans la boutique du jeu), si tous les joueurs avaient leurs équipements (armure ou costumes) de la même couleur, le monde serait bien terne et tout le monde ressemblerait à tout le monde.
À noter que contrairement à Guild Wars1, Guild Wars 2 ne permet pas quant à lui de teindre les armes (ce qui a été confirmé par un développeur comme faisant partie d'un choix de design pris lors de la phase de préconception sur lequel il est difficile, voir impossible de revenir). De plus, les dorsales et autres sac à dos qui sont des éléments introduits après la sortie du jeu ou encore les masques de plongée ne sont pas non-plus teignables. Il n'en reste pas moins que les armures et costumes, eux, le sont. Chacune des 6 pièces d'armure (ou chaque costume dans son intégralité) pouvant recevoir de 1 a 4 couleurs différentes (en fait les textures plaquées sur ces objets définissent des canaux qui peuvent recevoir des couleurs ; mais c'est une autre histoire).
Il existe plusieurs centaines de teintures en jeu sachant qu'ArenaNet continue d'en créer de nouvelles qui sont régulièrement distribuées via des packs exclusifs disponibles dans la boutique en ligne du jeu. Chaque teinture est en fait définie par 3 objets différents. Il existe en effet 3 types d'armures en jeu :
- Les armures légères pour les classes dites "érudits" (c'est à dire les lanceurs de sort) : les Envoûteurs, Élémentaliste ou Nécromants portent du tissu.
- Les armures moyennes pour les classes dites "aventuriers" : les Ingénieurs, Rôdeurs et Voleurs portent du cuir.
- Les armures lourdes pour les classes dites "soldats" : les Gardiens, Guerriers et les futurs Revenant (dans la future extension) portent du métal.
Une même teinture appliquée sur un type de matériau donné peut apparaitre de manière totalement différente par rapport à un autre matériau : couleur plus terne ou plus vive, plus claire, plus foncée, plus forte spécularité (comment la lumière se reflète dessus), etc. Certains de ces aspects sont propres à la pièce d'armure et d'autres à l'objet teinture. C'est pourquoi chaque teinture définit ces trois matériaux. Nous allons donc être amenés à manipuler un nombre plus conséquent d'objets que ce que nous avons fait jusqu’à présent.
Stratégies
Actuellement, il y a plus de 470 teintures disponibles en jeu. Récupérer l'ensemble des informations va donc nous demander une petite planification et, donc, nous allons observer de plus près les différentes options qui s'offrent à nous. Attention cependant, ici, je ne m’intéresse qu'aux stratégies possibles pour effectuer des requêtes sur le site web. Je ne me pencherai pas sur les stratégies possible pour mettre à jour l'affichage dans l'interface graphique. Ainsi, quelque soit la stratégie de requête choisie, notre Task retournera toujours une List<Node> "complète", c'est à dire qui contiendra les nœuds permettant d'afficher chacune des 470+ teintures à l’écran :
- Récupérer la liste des identifiants puis récupérer chacune des teintures une à une : cette stratégie naïve est la première que j'ai mise en place. C'est une extension de ce que nous avions fait lorsque nous avions récupéré des images de Quaggans.
Nous allons effectuer les actions suivantes :
- Récupération de la liste de tous les identifiants des teintures.
- Pour chaque identifiant de teinture :
- Récupération du descriptif de la teinture.
- Création de sa représentation à l'écran.
Cette stratégie, très simple à mettre en place, a un énorme défaut : le nombre de requêtes web. En plus de la requête initiale pour avoir la liste des identifiants, nous allons effectuer une requête web pour chacune des teintures dont l'identifiant est dans la liste. Cela amène deux conséquences :
- Cette tache est très très très longue à s’exécuter.
- Compte tenu du nombre de requête effectuées, cette stratégie est complètement vulnérable aux coupures du net et autres erreurs réseau.
- Récupérer la liste des teintures complète en une seule fois : dès que les défauts de la première stratégie me sont apparus (principalement le très long temps d’exécution de la tache), j'ai rapidement implémenté cette seconde stratégie qui tient plus de la requête de type "force brute", c'est à dire qu'ici on récupère tout en une seule requête (ce que nous avons fait précédemment pour la liste des mondes par exemple).
Nous allons effectuer les actions suivantes :
- Récupération de la liste de toutes les teintures.
- Pour chaque teinture :
- Création de sa représentation à l'écran.
- Création de sa représentation à l'écran.
Cette stratégie est également très simple à mettre en place. Ici, le nombre de requêtes est conservé à son minimum, c'est a dire 1 seule ! Le temps d’exécution est également généralement très rapide ce qui en fait une stratégie de choix. En fait dans cette stratégie, le problème se situe dans la taille des données récupérées. Ici la taille du texte retourne par la requête JSON est d'environ 180 Ko ! Cela ne parait pas beaucoup en l’état, certes, mais il est facile d’imaginer ce que cela serait si on tentait d'obtenir des informations beaucoup plus complexes et en bien plus grande quantité comme par exemple sur un magasin en ligne : en général on évite de récupérer l’intégralité du contenu de l'inventaire du magasin et on va plutôt tendance à récupérer seulement une page contenant les 10, 20, 50 ou 100 premiers articles de l'inventaire. - Récupérer la liste des identifiants puis récupérer les teintures par lot : cette dernière stratégie se repose justement sur un traitement par lot. C'est à dire que plutôt que de récupérer les teintures une à une comme dans la stratégie #1, ici, nous allons les récupérer par lot d'une certaine taille (10, 20, 50, 100).
Nous allons effectuer les actions suivantes :
- Récupération de la liste de tous les identifiants des teintures.
- Pour chaque lot d'identifiant de teinture :
- Récupération de la liste de toutes les teintures de ce lot.
- Pour chaque teinture de ce lot :
- Création de sa représentation à l'écran.
- Création de sa représentation à l'écran.
Performance
Voici le résultat de quelques tests de performances réalisés sur le tas sur chacune des stratégies. Évidement, cela a été testé sur la connexion toute pourrie de Nouvelle Calédonie aux heures de pointes. Il est probable que vos temps de réponse seront bien meilleurs que les miens.
Stratégie | Nombre de teintures | Nombre de requêtes | Poids du résultat (indicatif) | Temps total d’exécution | Temps requête liste d'identifiants | Temps moyen requête teinture | Temps maximum requête teinture |
---|---|---|---|---|---|---|---|
Stratégie #1 | 474 | 475 = 1 + 474 | 1Ko | 161617 ms | 2263 ms | 336 ms | 3515 ms |
Stratégie #2 | 474 | 1 | 180Ko | 3740 ms | n/a | 3423 ms | 3423 ms |
Stratégie #3 (lot de 10) | 474 | 49 = 1 + 48 | 4Ko | 20032 ms | 4348 ms | 33 ms | 693 ms |
Stratégie #3 (lot de 20) | 474 | 25 = 1 + 24 | 8Ko | 11265 ms | 2008 ms | 20 ms | 648 ms |
Stratégie #3 (lot de 50) | 474 | 11 = 1 + 10 | 19Ko | 6276 ms | 1985 ms | 9 ms | 794 ms |
Stratégie #3 (lot de 100) | 474 | 6 = 1 + 5 | 38Ko | 4497 ms | 1924 ms | 5 ms | 877 ms |
La tâche implémentant la stratégie #2, au contraire, s'exécute en environ 3 secondes dont la majorité est passé à récupérer le résultat d'une seule et unique requête.
Quant à la tâche implémentait la stratégie #3, ses performances restent de très bonne qualité, d'autant plus qu'on augmente la taille du lot de teintures à récupérer. Ses performances ne sont donc pas si éloignées de celle de la stratégie #2.
En détails
Il est possible de faire des requêtes sur l'endpoint colors qui est une nouvelle version de l'endoint du même nom dans la version 1 de l'API. La principale nouveauté est qu'il est désormais possible de retirer des informations de plusieurs teintures à la fois. Nous allons effectuer nos requêtes via l'URL https://api.guildwars2.com/v2/colors. Une requête sans paramètre sur ce point d’entrée retournera un tableau JSON contenant des identifiants numériques, à raison d'un identifiant par teinture. Tous les indices ne sont pas encore affectés à des teintures et il y a des trous dans la séquence.
Code JSON : | Sélectionner tout |
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,[...],1310,1311,1312,1333,1334,1335,1336,1337,1338,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357]
Une fois les identifiants récupérés, il est possible de faire des requêtes pour obtenir le détails des différentes teintures. Il est, de plus, possible d’utiliser le paramètre lang pour spécifier la langue dans laquelle le nom de la teinture est retourné.
- https://api.guildwars2.com/v2/colors/<id de la teinture>?lang=<code du language> - Renvoie un objet JSON contenant les informations de la teinture spécifiée.
- https://api.guildwars2.com/v2/colors?id=<id de la teinture>&lang=<code du language> - même chose.
- https://api.guildwars2.com/v2/colors?ids=<id de la teinture 1>,<id de la teinture 1>,...,<id de la teinture N>&lang=<code du language> - Renvoie un tableau JSON contenant les informations des teintures spécifiées. La valeur all pour le paramètre ids permet de retourner toutes les teintures.
Dans la V2 de l'API, il est également possible de faire des requêtes par page ; par exemple l'URL https://api.guildwars2.com/v2/colors?page=0&page_size=5 retournera la première page des valeur contenant 5 teintures. Cette solution peut-être utilisée pour éviter de charger l'intégralité des identifiants des teintures d'un seul coup et permet de faire un système de navigation similaire à ce qu'on peut trouver dans un moteur de recherche ou un magasin en ligne. Le nombre de pages total est sensé être contenu dans l'entête de la réponse HTTP mais j'avoue ignorer totalement comment le récupérer coté JSON. Évidement si on a récupéré la liste des identifiants auparavant on connait alors le nombre de teintures et on peut bien sur calculer le nombre de pages manuellement, cependant on peut tout. En tout cas, si vous tentez d'accéder à une page qui n'existe pas, un message d'erreur sera renvoyé. Par exemple à l'heure actuelle, l'URL https://api.guildwars2.com/v2/colors...0&page_size=20 renvoie l'erreur suivante dans un objet JSON :
Code JSON : | Sélectionner tout |
{"text":"page out of range. Use page values 0 - 23."}
Comme mon interface graphique n'est pas prévue pour une navigation page à page (je veux afficher toutes les couleurs), je vais mettre cette solution de côté mais gardez en tête qu'elle peut s'avérer utile pour naviguer à moindre coût dans un grand volume de données ou lorsqu'on fait des recherches.
Ici, je vais implémenter mon chargement des teintures par lot en utilisant la requête via le paramètre ids, mais j'aurai pu cependant l'implémenter également par un chargement par page pour peu que j'y ajoute une gestion d'erreur pour détecter la dernière page.
Vous connaissez à présent la musique ; si je tente d'accéder à l'URL https://api.guildwars2.com/v2/colors?id=1&lang=fr, le serveur retournera le descriptif de la toute première teinture à savoir le dissolvant à teinture (la couleur neutre) :
Code JSON : | Sélectionner tout |
{"id":1,"name":"Dissolvant pour teinture","base_rgb":[128,26,26],"cloth":{"brightness":15,"contrast":1.25,"hue":38,"saturation":0.28125,"lightness":1.44531,"rgb":[124,108,83]},"leather":{"brightness":-8,"contrast":1,"hue":34,"saturation":0.3125,"lightness":1.09375,"rgb":[65,49,29]},"metal":{"brightness":5,"contrast":1.05469,"hue":38,"saturation":0.101563,"lightness":1.36719,"rgb":[96,91,83]}}
Nettoyons un peu la chaîne retournée pour la rendre plus lisible :
Code JSON : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | { "id":1, "name":"Dissolvant pour teinture", "base_rgb":[128,26,26], "cloth":{ "brightness":15, "contrast":1.25, "hue":38, "saturation":0.28125, "lightness":1.44531, "rgb":[124,108,83] }, "leather":{ "brightness":-8, "contrast":1, "hue":34, "saturation":0.3125, "lightness":1.09375, "rgb":[65,49,29] }, "metal":{ "brightness":5, "contrast":1.05469, "hue":38, "saturation":0.101563, "lightness":1.36719, "rgb":[96,91,83] } } |
Nous pouvons voir que l'objet reçu est déjà plus volumineux et complexe que ce que nous avons précédemment manipulé. Comme indiqué plus haut, il existe 3 autres objets dans la définition de la teinture, un pour chacun des matériaux des armures.
Cooodage
Je vais commencer par coder le stockage de la teinture et du matériau en créant des classes simples et de structure similaires à ce qui est retourné au format JSON :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | public final class Dye { int id; String name; Color base; Material cloth; Material leather; Material metal; Dye() { } public int getId() { return id; } public String getName() { return name; } public Color getBase() { return base; } public Material getCloth() { return cloth; } public Material getLeather() { return leather; } public Material getMetal() { return metal; } } |
Pour compléter cette première classe, il me faut également définir les matériaux.
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public final class Material { int brightness; double contrast; Color color; Material() { } public int getBrightness() { return brightness; } public double getContrast() { return contrast; } public Color getColor() { return color; } } |
Vous remarquerez sans doute que j'ai omis une partie de la définition de la couleur du matériau. En effet, étant donné qu'on a déjà la couleur au format RGB (Red Green Blue - Rouge Vert Bleu ou RVB), j'ai jugé inutile pour le moment de stocker cette même couleur au format HSL (Hue Saturation Lightness - Teinte Saturation Luminosité ou TSL).
Vous remarquerez également que les membres et le constructeur sont en accès package-private. C'est normal car je vais placer une classe fabrique dans le même package. Cette classe se chargera d'instancier les deux classes et de remplir les valeurs des membres à partir des objets JSON qui lui sont fournis :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public enum DyeFactory { INSTANCE; public static Dye createDyeFromJSON(final JsonObject jsonObject) { final Dye result = new Dye(); result.id = jsonObject.getInt("id"); // NOI18N. result.name = jsonObject.getString("name"); // NOI18N. final JsonArray base_rgb = jsonObject.getJsonArray("base_rgb"); // NOI18N. final int red = base_rgb.getInt(0); final int green = base_rgb.getInt(1); final int blue = base_rgb.getInt(2); result.base = Color.rgb(red, green, blue); result.cloth = createMaterialFromJSON(jsonObject.getJsonObject("cloth")); // NOI18N. result.leather = createMaterialFromJSON(jsonObject.getJsonObject("leather")); // NOI18N. result.metal = createMaterialFromJSON(jsonObject.getJsonObject("metal")); // NOI18N. return result; } private static Material createMaterialFromJSON(final JsonObject jsonObject) { final Material result = new Material(); result.brightness = jsonObject.getJsonNumber("brightness").intValue(); // NOI18N. result.contrast = jsonObject.getJsonNumber("contrast").doubleValue(); // NOI18N. final JsonArray rgb = jsonObject.getJsonArray("rgb"); // NOI18N. final int red = rgb.getInt(0); final int green = rgb.getInt(1); final int blue = rgb.getInt(2); result.color = Color.rgb(red, green, blue); return result; } } |
Cette fabrique dispose donc de deux méthodes : createDyeFromJSON() pour créer une teinture à partir d'un objet JSON et createMaterialFromJSON() pour créer un matériau à partir d'un objet JSON. Seule la première est publique et c'est celle qui sera invoquée depuis notre objet servant à faire les requêtes.
L'objet requête, d'ailleurs, reste très simple et est similaire à ce que nous avons précédemment fait. C'est lui qui va récupérer les objets et tableaux JSON grâce à l'API JSON-P et qui va ensuite invoquer la fabrique pour générer les objets Dye (au fait, dye veut dire teinture en anglais) :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public enum ColorsQuery { INSTANCE; private static final String BASECODE = "https://api.guildwars2.com/v2/colors"; // NOI18N. public static List<Integer> list() throws IOException { final JsonArray array = queryArray(BASECODE ); final List<Integer> result = array.getValuesAs(JsonNumber.class) .stream() .map(value -> value.intValue()) .collect(Collectors.toList()); return Collections.unmodifiableList(result); } public static Dye dyeInfo(final String languageCode, final int id) throws IOException { final JsonObject value = QueryUtils.queryObject(String.format("%s/%d?lang=%s", BASECODE, id, languageCode)); // NOI18N. final Dye result = DyeFactory.createDyeFromJSON(value); return result; } public static List<Dye> dyeInfos(final String languageCode, final int... ids) throws IOException { final String idsCode = idsToString(ids); final JsonArray array = queryArray(String.format("%s?ids=%s&lang=%s", BASECODE, idsCode, languageCode)); // NOI18N. final List<Dye> result = array.getValuesAs(JsonObject.class) .stream() .map(value -> DyeFactory.createDyeFromJSON(value)) .collect(Collectors.toList()); return Collections.unmodifiableList(result); } } |
Il y a une première méthode pour récupérer la liste de tous les identifiants de teintures, une seconde pour récupérer les détails d'une teinture et une dernière pour récupérer les informations d'un lot de teintures. Hum, on commence à voir que le traitement et la manipulation de flux des tableau pour la conversion du contenu JsonString ou JsonObject en objet Java exploitable (Integer ou Dye) est relativement similaire part rapport à ce que nous avons fait la dernière fois. Je pense qu'il y a moyen de simplifier cela par l'ajout de quelques méthodes dans notre classe utilitaire QueryUtils ; il faudra que j'y songe pour la prochaine fois.
Le reste du code concerne l'UI en JavaFX ce qui s'éloigne donc des préoccupations premières de ces posts et donc je vais vous laisser découvrir cela en lisant les sources du contrôleur du FXML. Comme précédemment c'est dans cette classe que se trouve toute la logique de l'interface graphique. Simplement, lorsque les teintures sont récupérées, nous allons créer des nœuds graphiques pour afficher les couleurs à l'écran conformément aux stratégies présentées plus haut. En ce qui concerne la présentation à l'écran : un simple rectangle coloré suffira. Un écouteur de souris est placé sur chacun de ces nœuds pour que, lors d'un clic, les détails de la couleur soient affichés en bas d'écran. D'autres contrôles dans l'UI permettent de changer la langue (ce qui implique de re-télécharger toutes les teintures comme c'était le cas avec la liste des mondes) ou encore de changer le matériau affiché (ce qui fait que chaque nœud se met à jour à la bonne couleur).
Voilà ce à quoi ressemble le programme final :
N'oubliez pas qu'une fois les teintures complètement chargées, vous pouvez cliquer sur une des cellules pour avoir les détails des 3 matériaux affichés en bas de l'écran. Parfois la différence entre un matériau et un autre est assez subtile mais c'est largement suffisant pour créer une palette de couleur plus riche en jeu.
Vous pouvez également aller voir dans le fichier CSS pour trouver comment j'ai changer l'apparence et la couleur de la barre de progression (je lui ai donné une apparence proche de celle du launcher du jeu).
Code source
vous retrouverez l’exemple au complet dans l'archive ci-jointe : [ATTACH]179376d1/a/a/a" />
La version la plus récente du projet, au format NetBeans, est accessible sur GitHub.