Tous les articles

L'écosystème complet : d'un jeu live à une web-app d'optimisation de builds

29 juin 20267 min de lecture

Aider un joueur à comparer deux builds d'équipement suppose un prérequis qu'on oublie facilement en regardant l'interface finie : il faut des données de jeu propres, complètes et à jour. Pas juste les stats des objets, mais leurs traductions, leurs sprites, leurs relations entre eux, resynchronisées à chaque patch du jeu.

Ce prérequis, à lui seul, a fini par dessiner cinq systèmes distincts avant même d'écrire la première ligne de l'interface de comparaison. Cet article fait le tour du propriétaire : comment ces cinq briques s'articulent, et pourquoi elles sont restées séparées plutôt que fondues en un seul projet.

Avant de rentrer dans le détail de chaque brique, un schéma simplifié vaut mieux qu'un long paragraphe. Voici le pipeline en une vue, du client du jeu jusqu'au navigateur :

Client du jeu
binaire opaque
Décodeur
suit la dérive
Hub ETL
agrège, versionne
Web-app + rendu
Postgres, Rust
Navigateur
builds comparés
Un numéro de version se propage dans un seul sens, du jeu jusqu'au navigateur.

Brique 1 : décoder ce que le jeu ne documente pas

Tout commence par un problème d'acquisition. Le client d'un MMO live stocke ses données dans des fichiers binaires positionnels, sans balises ni noms de champs, et ce format dérive légèrement à chaque patch. Avant de pouvoir afficher quoi que ce soit côté produit, il faut d'abord un outil capable de suivre cette dérive de version en version, sans réécrire le décodeur à la main à chaque mise à jour. Le même problème se pose pour les tooltips du jeu, qui ne sont pas du texte brut mais un petit DSL avec ses propres références et sa propre logique de mise en forme. Cette brique reste volontairement la plus fermée du lot : c'est celle qui touche directement au client du jeu.

Brique 2 : agréger, verser, distinguer ce qui est public

Une fois les données extraites, encore faut-il les rendre exploitables. Le hub ETL prend les sorties brutes de la brique précédente, les recoupe, les enrichit, et les publie sous deux formes : des artefacts JSON versionnés d'un côté, une base Postgres alimentée de l'autre. C'est aussi à cet étage que se joue une distinction qui traverse tout le reste du pipeline : certaines données sont publiques (celles qui finissent dans le produit), d'autres restent internes (celles qui servent au pipeline lui-même). L'organisation du code de ce hub suit une architecture hexagonale assez stricte, pour que les adaptateurs d'entrée, les formats bruts qui changent à chaque patch, restent découplés du cœur métier.

Brique 3 : rendre un sprite sans carte graphique

Un des artefacts produits par le hub ETL, ce sont les fichiers d'animation et les atlas de textures des personnages. Le service de rendu de sprites, écrit en Rust, prend ces artefacts et compose une image PNG à la demande, côté serveur, sans GPU disponible. La contrainte n'est pas de produire une jolie image mais de produire exactement la même image que le client du jeu afficherait, pixel pour pixel. Ce service tourne à part des autres parce que sa charge de travail, une rasterisation CPU intensive, et son cycle de vie n'ont rien à voir avec ceux d'un serveur web classique.

Brique 4 : le produit que voit le joueur

C'est la brique visible : une web-app en Next.js pour le front et AdonisJS pour l'API, qui permet à un joueur de construire un équipement, de le comparer à d'autres, et de le partager. Elle consomme la base Postgres alimentée par le hub ETL, interroge le service de rendu pour afficher les sprites, et s'appuie sur Redis pour le cache des lectures les plus fréquentes. Deux morceaux méritent leur article dédié : la façon dont la base se resynchronise avec un nouvel export sans jamais couper le service, et une recherche d'objets par teinte visuelle plutôt que par nom, construite sur une distance de couleur dans l'espace Lab.

Brique 5 : servir les images sans réinventer un CDN

Dernière brique, la plus modeste en taille de code mais pas en utilité : un petit service qui sert les images (icônes d'objets, sprites rendus, illustrations) avec redimensionnement et mise en cache, en s'arrêtant volontairement avant d'ajouter des fonctionnalités qu'un vrai CDN gère déjà mieux. Le choix du périmètre est ce qui rend ce service tenable à quatre-vingt-dix lignes plutôt qu'à quelques milliers.

Le fil qui relie les cinq

Ce qui rend ces cinq dépôts cohérents entre eux, ce n'est pas un déploiement commun ni un langage partagé (il y a du Java décompilé, du Rust, du TypeScript, du SQL), c'est un numéro de version qui se propage dans un seul sens.

  1. 1

    Un patch sort

    Le jeu publie un patch, ce qui déclenche la brique 1.
  2. 2

    Décodage versionné

    La brique 1 produit de nouveaux artefacts versionnés.
  3. 3

    Agrégation

    Le hub ETL les consomme et publie sa propre version d'agrégat.
  4. 4

    Consommation

    Le rendu et la web-app tirent chacun la version d'artefacts qui les concerne.
  5. 5

    Reseed

    Jusqu'au reseed de la base produit.

À aucun moment une brique en aval ne devine ou ne recalcule une donnée qu'une brique en amont aurait dû lui fournir déjà résolue.

Ce que ces cinq briques m'ont appris

La tentation, au départ, aurait été de tout mettre dans un seul dépôt monolithique : après tout, c'est le même projet. Mais chaque brique a un rythme de changement, un langage et un profil de charge totalement différents. Le décodeur binaire ne tourne que lorsqu'un patch sort. Le service de rendu tourne en continu mais n'a besoin que de très peu de mémoire persistante. La web-app, elle, a le rythme d'un produit avec des utilisateurs en direct. Les forcer dans un même déploiement aurait couplé leurs cycles de vie sans raison.

À retenir

Séparer ces briques a forcé une discipline utile : chaque frontière entre deux dépôts est une interface explicite, avec un format de données versionné, qu'on ne peut pas contourner en important discrètement une fonction interne d'un autre module.

Cette discipline, appliquée à un contexte où on ne maîtrise ni le format source ni son rythme de changement, est le fil conducteur de tout le reste.