HTML 5, Javascript, etc... Tout cela ne vous dit rien ? Heureusement alors, que
l'article est écrit de sorte à être compréhensible de tous. Si Riot communique beaucoup sur le nouveau client, cet aspect de compréhension n'est pas négligé pour autant. Et ici il s'agit de parler de ce qu'utilise le nouveau client pour fonctionner.
Au cœur de League : l'architecture de la mise à jour du client
Salutations ! Je m'appelle Andrew McVeigh, et je suis architecte logiciel pour Riot Games.
Nous sommes actuellement dans les dernières phases de développement du nouveau client de League of Legends. C'est la fameuse mise à jour du client de League. Dans ce billet, je vais vous détailler l'architecture logicielle de cette mise à jour et vous expliquer un peu les choix que nous avons faits en soulignant certaines des limitations du client actuel. D'un point de vue technique, ce voyage vers l'architecture finale a été fascinant, et je suis très heureux de pouvoir le partager avec vous !
Pourquoi remplacer le client ?
Nous avons construit notre client en 2008 à l'aide d'une technologie front-end appelée
Adobe AIR, qui utilise un protocole réseau
RTMP pour communiquer avec nos serveurs. Cette plateforme nous a été très utile : elle nous offrait une exécution multimédia très riche avec des animations et des effets qu'il était impossible d'obtenir à l'époque avec du code HTML. De plus, c'était un outil multiplateforme et notre équipe a pu facilement construire les écrans alors qu'elle comportait peu d'artistes et de concepteurs.
Petit bon en avant de sept ans et nous arrivons en 2015 : trois problèmes particuliers du client AIR sont devenus assez inquiétants. C'est là que notre nouvelle architecture entre en jeu pour corriger ces trois problèmes, entre autres choses.
Problème N°1 - HTML 5 est une plateforme standardisée largement répandue
Le HTML5 pur et JavaScript sont devenus des technologies viables pour un client bureau. Ceci présente de nombreux avantages, dont les flux de travail et les outils de développement standardisés, ainsi qu'un large panel de développeurs de talent. Nous tenons à maîtriser pleinement cette plateforme et les intégrations de média qu'elle permet de mettre en place.
Problème N°2 - Les joueurs qui souhaitent rester connectés au jeu en permanence
Vous ne voudriez pas laisser le client AIR tourner constamment, même en arrière-plan, vu le nombre faramineux de ressources qu'il nécessite. Et pourtant, les attentes des joueurs en matière de connectivité ont radicalement évolué au fil des dernières années. Ils souhaitent rester connectés avec leurs amis dans le jeu et en-dehors - on ne devrait pas manquer une invitation à jouer juste parce qu'on ne se trouve pas devant son PC avec le client ouvert (nous travaillons également à corriger ce souci avec
notre appli mobile).
Problème N°3 - Les équipes de développement de Riot veulent travailler en harmonie
League (tout comme Riot) a énormément grandi depuis 2008 et nous n'aurions jamais pensé que nous nous retrouverions avec autant d'équipes différentes qui veulent toutes ajouter diverses fonctionnalités au client. La base de code originelle était structurée autour d'une équipe de taille réduite et très soudée. À mesure que le nombre de fonctionnalités du jeu a grandi, ce code n'offrait plus le niveau requis d'autonomie et d'indépendance aux équipes toujours plus nombreuses.
Ce problème des diverses équipes de développement qui se gênaient les unes les autres n'allait pas se résoudre de lui-même - il ne faisait qu'empirer à mesure que nous ajoutions de nouvelles fonctionnalités. Par ailleurs, nous cherchons à devenir un studio qui produit plusieurs jeux, ce qui implique de nombreux autres défis et autant d'opportunités nouvelles.
Avancer vers une architecture spectaculaire
Il existe de multiples solutions possibles pour corriger les problèmes cités précédemment : par exemple, nous avons passé beaucoup de temps à réorganiser le code AIR existant en essayant de conserver le navigateur AIR (pourtant obsolète). Cependant, même si nous avons exploré cette option en profondeur, nous avons compris que nous pourrions résoudre les problèmes mentionnés de façon plus efficace et durable en changeant de plateforme. Nous avons opté pour cette option même si nous étions pleinement conscients des
dangers d'une réécriture, parce que nous pensons que les bénéfices pour les joueurs vont contrebalancer les risques sur le long terme.
Nous avons donc décidé de tout recommencer et d'utiliser le
Chromium Embedded Framework (CEF)comme technologie front-end pour construire le client. Ceci nous permet de tirer parti d'un navigateur HTML5 très performant que nous pouvons aisément personnaliser.
À présent, laissez-moi vous expliquer comment nous nous sommes frayé un chemin vers notre architecture finale.
Étape A : Javascript est le roi du monde !
Au départ, notre idée était de tout faire avec JavaScript.
Puisque nous avions prévu de construire l'interface utilisateur en utilisant HTML et JavaScript, à l'origine, nous pensions qu'en intégrant toute la logique de travail et de communication dans JavaScript, nous pourrions simplifier l'architecture et la rendre plus uniforme. (L'impulsion pour ce type d'uniformité entre l'interface utilisateur et les services se situe au cœur de plateformes comme
node.js)
Nous avons donc développé une bibliothèque C++ simple et minimale qui permet à JavaScript de contacter le serveur à distance par protocole RTMP et de gérer des réponses asynchrones. Nous avons conservé le protocole RTMP parce qu'il fonctionne bien à notre échelle actuelle, et parce que nous ne voulions pas changer le protocole de communication au même moment que nous changeons le client.
« Qu'est-ce qui pourrait bien aller de travers ? », nous sommes-nous dit. Eh bien... il se trouve que pas mal de choses sont allées de travers.
Notre code JavaScript est devenu extrêmement complexe parce que nous gérions toutes les réponses asynchrones au niveau du Web Tier. De plus, l'état des joueurs était conservé dans JavaScript également, ce qui signifie que nous ne pouvions pas facilement réduire l'empreinte mémoire.
Cette nouvelle architecture a donc résolu le problème n°1, mais n'a rien pu faire pour les problèmes n°2 et 3. Une enquête de satisfaction interne parmi les développeurs nous a permis de nous rendre compte qu'il était moins efficace de procéder au développement de cette façon qu'avec le client AIR. Aïe.
Étape B : Nous avons redécouvert les applications web (Mais sur le bureau !)
L'étape suivante était de fournir des microservices en C++ et de présenter le protocole de jeu asynchrone en tant qu'ensemble de ressources REST.
Nous avons commencé à penser à la façon dont sont conçues les applications web normales et nous nous sommes rendu compte qu'il nous manquait un niveau intermédiaire. Nous avons construit une couche de microservice (qui tourne toujours sur le bureau du joueur) afin de présenter le protocole RTMP comme des
ressources REST, pour isoler JavaScript de la masse de réponses asynchrones. Nous avons utilisé des websockets pour la gestion de l'interface utilisateur. Nous avons appelé cette couche de microservices la « fondation ».
Voici une image qui montre
l'interface utilisateur Swagger pour certaines des ressources du patcher de la fondation. Cette configuration a grandement simplifié le code JavaScript, et les développeurs ont été en mesure d'utiliser des techniques web standard.

Nous avons également découvert que cette architecture présentait un autre avantage : quand nous avons réduit la fenêtre du client, nous avons été en mesure d'interrompre le processus de l'interface utilisateur CEF (et toutes les machines virtuelles JavaScript), en ne conservant que la fondation. Cela n'a été possible que parce que la fondation conserve un état canonique - l'interface utilisateur tout entière peut être reconstituée avec des GET. La fondation n'utilise que 20mb de mémoire environ, ce qui est approximativement la taille de quelques images de chats trouvées sur internet. Personne ne devrait avoir besoin d'interrompre le processus du client de nouveau après avoir lancé un match (comme le font actuellement certains joueurs pour économiser de la mémoire).
Ceci nous a permis de commencer à construire notre architecture en progressant vers notre vision d'un client « toujours disponible », ce qui résolvait le problème n°2. La fondation peut faire apparaître une notification, même en arrière-plan, pour vous indiquer que vous avez reçu une invitation à jouer ou une demande d'ami.
Étape C : Progression sur la pointe des pieds
L'étape suivante était d'avancer vers une architecture extensible.
Quand les équipes ont commencé à développer des microservices et de nouvelles fonctionnalités, un nouveau problème a fait surface. Ces équipes mettaient souvent à jour le même code et se gênaient les unes les autres.
Utilisons une analogie pour bien expliquer la notion d'extensibilité. Imaginez que dix personnes doivent écrire un seul et même livre. Supposons qu'on leur demande de travailler sur chaque chapitre ensemble (par exemple, chapitre 2 : « Débuter dans League of Legends »). Ce serait un vrai cauchemar, parce que chaque auteur essaierait de modifier les phrases des autres. Il est beaucoup plus logique de leur donner à chacun un chapitre à rédiger (par exemple, chapitre 5 : « Maîtrises » et chapitre 6 : « Runes »), ce qui leur permettra de travailler de façon indépendante sur le même livre. Bien sûr, la difficulté est de faire en sorte que le livre final soit cohérent.
Nous avons dû relever un défi similaire avec la mise à jour du client. Les équipes ajoutaient toutes leurs fonctionnalités, mais trop souvent, elles devaient ajuster des lignes de codes et des frameworks partagés. Il nous fallait un schéma architectural qui leur permettrait de rester aussi indépendantes que possible.
J'ai passé beaucoup de temps à travailler sur
les architectures extensibles et je sais qu'elles permettent de corriger le problème des développeurs dont les travaux entrent en conflit les uns avec les autres. Pour que notre architecture reste simple, nous avons choisi une variante d'une
approche d'extensibilité qui a fait ses preuves : les plug-ins. Un plug-in est une unité indépendante de propriété, de développement, de test et de déploiement. Chaque plug-in respecte la
gestion sémantique de version et doit indiquer de quels autres plug-ins et versions il dépend, ce qui préserve la lisibilité de l'architecture. Nous avons développé cet aspect avec un graph de dépendance très explicite, qui indique comment les plug-ins sont connectés entre eux.
Ainsi, un écran ou une fonctionnalité de l'interface utilisateur est intégré dans ce que nous appelons un plug-in front-end (FE), et les communications C++ sont intégrées dans un plug-in back-end (BE). Il est à noter que les deux types de plug-ins sont présents dans le client mis à jour - nous avons utilisé les termes de front et back-end parce que notre architecture imite une architecture client-serveur classique, même si elle est intégrée à un seul et unique processus du bureau.
Nous pouvons ensuite permettre à l'ensemble des plug-ins choisis pour un joueur de s'appuyer sur les autorisations dont dispose ce joueur. Les autorisations sont utilisées pour contrôler les fonctionnalités spécifiques à chaque région, mais seront également utilisées pour donner accès à de nouveaux jeux.
Pendant la transition vers cette nouvelle architecture, nous avons dû déballer notre couche de données monolithique JavaScript. Nous utilisions ember-orbit pour maintenir un seul graph d'objets qui est connecté, et qui représentait nos ressources REST. Cela nous a valu quelques bonnes migraines, car cela signifiait que plusieurs équipes allaient encore avoir accès aux données d'autres équipes via des liaisons d'objet et non via des appels de service. Cela créait beaucoup trop d'interdépendances.
Nous avons donc cessé d'utiliser Orbit et opté pour un simple cache de ressources attaché à chaque plug-in front-end. Ceci nous a permis de grandement simplifier notre architecture.
Étape D : Demander à N développeurs Javascript quel était leur framework MVC favori. Obtenir N+1 réponses
Nous avons permis à chaque équipe en charge de créer des fonctionnalités nouvelles de choisir leur propre framework JavaScript.
Nous avions obtenu une architecture beaucoup plus productive et les équipes étaient en mesure de créer des écrans et des services rapidement, avec des responsabilités clairement réparties.
Malgré tout, nous étions face à un problème de développeur. Certaines des équipes n'aimaient pas du tout Ember.js et voulaient utiliser les composants web différemment. D'autres possédaient des applis web déjà constituées dans une large gamme de frameworks qu'ils souhaitaient intégrer à notre nouveau client.
À ce stade, nous hésitions encore à abandonner la règle du « Ember.js pour tous », surtout à cause d'une simple préférence des développeurs. Si Ember n'était plus le seul et unique framework, nous allions avoir beaucoup de travail supplémentaire.
Le tournant capital s'est produit lorsque nous avons compris que sur la plateforme client d'une entreprise de la taille de Riot, il serait impossible d'imposer à tout le monde ne serait-ce que la même versiond'Ember. C'était une décision difficile, mais nous avons fini par construire l'architecture du client de façon à ce que chaque plug-in puisse potentiellement avoir son propre framework - le framework JavaScript faisait désormais partie du domaine de chaque plug-in front-end.
Notez l'importance du mot potentiellement dans cette phrase. Nous utilisons toujours Ember pour la majorité de nos fonctionnalités (question de cohérence !), mais au niveau technique, ce n'est plus une nécessité absolue.
L'architecture finale
L'architecture du client mis à jour nous permet désormais de résoudre les trois problèmes d'origine que nous avons évoqués. C'est un moteur qui permet à plusieurs équipes d'implémenter des fonctionnalités construites avec HTML5, de façon indépendante, sans interconnexions inutiles, en utilisant une infrastructure de communications de soutien grâce à laquelle les joueurs peuvent rester connectés. Dans les faits, c'est un moteur d'hébergement pour des microservices C++ et des applis web JavaScript, où le choix des plug-ins utilisés est personnalisé et dynamique, basé sur les autorisations de chaque joueur.
Nous avons de nouveau lancé une enquête de satisfaction auprès des développeurs, comme mentionné plus haut, et nous avons découvert que la situation s'était améliorée d'environ 15% et continuait de progresser en bien. Que du bonheur !
L'un des développeurs a indiqué en commentaire que nous avions créé « un centre de données pour bureau ». D'un autre point de vue, j'ai compris que nous avions créé l'équivalent dans les jeux vidéo de l'outil
Atom, l'éditeur de texte extensible construit par Github avec le CEF, et qui s'appuie sur le framework
Electron. Plutôt intéressant... Je n'aurais jamais pensé que nous aboutirions à ce genre de résultat quand nous avons commencé à travailler.
Il y a de nombreuses questions que nous n'avons pas pu aborder explicitement dans cet article : par exemple, comment nous assurer que les fonctionnalités interagissent sans encombre et ne se retrouvent pas mêlées les unes aux autres ? Comment obtenons-nous des effets de qualité dans le jeu en utilisant HTML5 ? Et comment parvenons-nous à déployer des fonctionnalités de façon indépendante ? Tous ces sujets sont fascinants, et nous serons ravis d'en discuter avec vous s'ils vous intéressent vraiment.