Découvrez le potentiel de WebGL avec les paysages infinis générés de manière procédurale de ce jeu de conduite occasionnel.
Slow Roads est un jeu de conduite occasionnel mettant l'accent sur des paysages générés de manière procédurale sans fin, tous hébergés dans le navigateur en tant qu'application WebGL. Pour beaucoup, une expérience aussi intensive peut sembler déplacée dans le contexte limité du navigateur. C'est d'ailleurs l'un de mes objectifs avec ce projet. Dans cet article, je vais expliquer certaines des techniques que j'ai utilisées pour surmonter l'obstacle des performances dans ma mission visant à mettre en avant le potentiel souvent négligé de la 3D sur le Web.
Développement 3D dans le navigateur
Après avoir lancé les routes à faible vitesse, j'ai remarqué un commentaire récurrent dans les commentaires: "Je ne savais pas que c'était possible dans le navigateur". Si vous partagez ce sentiment, vous n'êtes certainement pas une minorité. Selon l'enquête État de JS 2022, 80% des développeurs n'ont pas encore expérimenté WebGL. Je trouve dommage que tant de potentiel puisse être gâché, en particulier en ce qui concerne les jeux basés sur un navigateur. Avec Slow Roads, j'espère mettre WebGL sous le feu des projecteurs et peut-être réduire le nombre de développeurs qui rechignent à utiliser l'expression "moteur de jeu JavaScript hautes performances".
WebGL peut sembler mystérieux et complexe pour beaucoup, mais au cours des dernières années, ses écosystèmes de développement ont beaucoup évolué pour devenir des outils et des bibliothèques très performants et pratiques. Il est désormais plus facile que jamais pour les développeurs front-end d'intégrer l'expérience utilisateur 3D à leur travail, même sans expérience préalable en infographie. Three.js, la principale bibliothèque WebGL, sert de base à de nombreuses extensions, y compris react-three-fiber, qui intègre des composants 3D au framework React. Il existe désormais également des éditeurs de jeux Web complets tels que Babylon.js ou PlayCanvas, qui offrent une interface familière et des chaînes d'outils intégrées.
Cependant, malgré l'utilité remarquable de ces bibliothèques, les projets ambitieux sont finalement soumis à des limites techniques. Les sceptiques quant à l'idée de jeux basés sur le navigateur peuvent souligner que JavaScript est monothread et limité en ressources. Mais surmonter ces limites permet de découvrir la valeur cachée: aucune autre plate-forme n'offre la même accessibilité instantanée et la même compatibilité massive que le navigateur. Les utilisateurs de n'importe quel système compatible avec un navigateur peuvent commencer à jouer en un clic, sans avoir à installer d'applications ni à se connecter à des services. Sans compter que les développeurs bénéficient de la commodité élégante de disposer de frameworks front-end robustes pour créer une interface utilisateur ou gérer la mise en réseau pour les modes multijoueurs. À mon avis, ce sont ces valeurs qui font du navigateur une excellente plate-forme pour les joueurs et les développeurs. Comme Slow Roads l'a démontré, les limites techniques peuvent souvent être réduites à un problème de conception.
Obtenir des performances fluides sur les routes à circulation lente
Étant donné que les éléments principaux de Slow Roads impliquent un mouvement à grande vitesse et une génération de paysages coûteuse, le besoin de performances fluides a sous-tendu chacune de mes décisions de conception. Ma stratégie principale était de commencer par une conception de gameplay épurée qui permettait d'utiliser des raccourcis contextuels dans l'architecture du moteur. En contrepartie, cela implique de renoncer à certaines fonctionnalités souhaitables dans le but de privilégier le minimalisme. En revanche, vous bénéficiez d'un système personnalisé et hyper-optimisé qui fonctionne bien sur différents navigateurs et appareils.
Voici un aperçu des principaux composants qui permettent de garder les routes lentes légères.
Adapter le moteur d'environnement au gameplay
En tant que composant clé du jeu, le moteur de génération d'environnement est inévitablement coûteux et occupe à juste titre la plus grande partie des budgets de mémoire et de calcul. L'astuce utilisée ici consiste à planifier et à distribuer le calcul lourd sur une période donnée, afin de ne pas interrompre le framerate avec des pics de performances.
L'environnement est composé de tuiles de géométrie, dont la taille et la résolution varient (catégorisées en "niveaux de détail" ou LoD) en fonction de la proximité avec la caméra. Dans les jeux classiques avec une caméra en mode libre, différents niveaux de détail doivent être constamment chargés et déchargés pour détailler l'environnement du joueur où qu'il aille. Cette opération peut être coûteuse et gaspilleuse, en particulier lorsque l'environnement lui-même est généré de manière dynamique. Heureusement, cette convention peut être entièrement subvertie dans les routes à faible vitesse grâce à l'attente contextuelle que l'utilisateur doit rester sur la route. À la place, la géométrie haute fidélité peut être réservée au couloir étroit qui borde directement l'itinéraire.

La ligne médiane de la route elle-même est générée bien avant l'arrivée du joueur, ce qui permet de prédire précisément quand et où les détails de l'environnement seront nécessaires. Le résultat est un système efficace qui peut planifier de manière proactive des tâches coûteuses, en générant uniquement le minimum nécessaire à chaque instant, sans effort inutile sur les détails qui ne seront pas visibles. Cette technique n'est possible que parce que la route est un chemin unique et sans embranchement, ce qui est un bon exemple de compromis de jeu qui s'accommode des raccourcis architecturaux.

Être pointilleux avec les lois de la physique
La simulation physique vient en deuxième position après la demande de calcul du moteur d'environnement. Slow Roads utilise un moteur physique personnalisé et minimal qui prend tous les raccourcis disponibles.
L'économie principale consiste à éviter de simuler trop d'objets en premier lieu, en privilégiant le contexte minimal et zen en ignorant des éléments tels que les collisions dynamiques et les objets destructibles. En supposant que le véhicule reste sur la route, les collisions avec des objets hors route peuvent raisonnablement être ignorées. De plus, l'encodage de la route en tant que ligne médiane sporadique permet d'utiliser des astuces élégantes pour détecter rapidement les collisions avec la surface de la route et les rails de protection, le tout basé sur une vérification de la distance par rapport au centre de la route. La conduite hors route devient alors plus coûteuse, mais il s'agit d'un autre exemple de compromis équitable adapté au contexte du jeu.
Gérer l'espace mémoire utilisé
Comme il s'agit d'une autre ressource limitée par le navigateur, il est important de gérer la mémoire avec soin, même si JavaScript est collecté. Il peut être facile de passer cela sous silence, mais déclarer même de petites quantités de mémoire dans une boucle de jeu peut entraîner des problèmes importants lors de l'exécution à 60 Hz. En plus d'utiliser les ressources de l'utilisateur dans un contexte où il est susceptible d'effectuer plusieurs tâches à la fois, les collectes de mémoire importantes peuvent prendre plusieurs images, ce qui entraîne des à-coups visibles. Pour éviter cela, la mémoire de la boucle peut être préallouée dans les variables de classe lors de l'initialisation et recyclée dans chaque frame.

Il est également très important que les structures de données plus lourdes, telles que les géométries et les tampons de données associés, soient gérées de manière économique. Dans un jeu généré à l'infini comme Slow Roads, la majeure partie de la géométrie existe sur une sorte de tapis roulant. Une fois qu'un ancien élément est loin derrière, ses structures de données peuvent être stockées et recyclées pour un élément à venir du monde, un modèle de conception appelé "pool d'objets".
Ces pratiques permettent de prioriser l'exécution allégée, au détriment de la simplicité du code. Dans les contextes hautes performances, il est important de tenir compte de la façon dont les fonctionnalités de commodité empruntent parfois au client pour le bénéfice du développeur. Par exemple, des méthodes telles que Object.keys()
ou Array.map()
sont extrêmement pratiques, mais il est facile de négliger le fait que chacune crée un tableau pour sa valeur renvoyée. Comprendre le fonctionnement interne de ces boîtes noires peut vous aider à resserrer votre code et à éviter les baisses de performances insidieuses.
Réduire le temps de chargement avec des éléments générés de manière procédurale
Bien que les performances d'exécution soient la principale préoccupation des développeurs de jeux, les axiomes habituels concernant le temps de chargement initial des pages Web restent valables. Les utilisateurs peuvent être plus indulgents lorsqu'ils accèdent volontairement à des contenus volumineux, mais les longs temps de chargement peuvent toujours nuire à l'expérience, voire à la fidélisation des utilisateurs. Les jeux nécessitent souvent de grands éléments sous la forme de textures, de sons et de modèles 3D. Ils doivent au minimum être compressés avec soin partout où des détails peuvent être supprimés.
Vous pouvez également générer des éléments de manière procédurale sur le client pour éviter les transferts longs. Cela représente un avantage considérable pour les utilisateurs disposant de connexions lentes et permet au développeur de contrôler plus directement la constitution de son jeu, non seulement pour l'étape de chargement initiale, mais aussi pour adapter les niveaux de détails aux différents paramètres de qualité.
La plupart de la géométrie des routes à circulation lente est générée de manière procédurale et simplifiée, avec des nuanceurs personnalisés combinant plusieurs textures pour apporter des détails. L'inconvénient est que ces textures peuvent être des éléments lourds, mais il existe d'autres possibilités d'économies ici, avec des méthodes telles que la texturation stochastique permettant d'obtenir plus de détails à partir de petites textures sources. À un niveau extrême, il est également possible de générer des textures entièrement sur le client à l'aide d'outils tels que texgen.js. Il en va de même pour l'audio, avec l'API Web Audio qui permet de générer du son avec des nœuds audio.
Grâce aux composants procéduraux, la génération de l'environnement initial ne prend que 3,2 secondes en moyenne. Pour tirer le meilleur parti de la petite taille de téléchargement initiale, un écran de démarrage simple accueille les nouveaux visiteurs et reporte l'initialisation coûteuse de la scène jusqu'à ce qu'un bouton d'acceptation soit enfoncé. Cela sert également de tampon pratique pour les sessions de rebond, ce qui réduit le gaspillage de transfert d'éléments chargés dynamiquement.
Adopter une approche agile pour l'optimisation tardive
J'ai toujours considéré le code de base de Slow Roads comme expérimental et, en tant que tel, j'ai adopté une approche extrêmement agile pour le développement. Lorsque vous travaillez avec une architecture système complexe et en rapide évolution, il peut être difficile de prédire où les goulots d'étranglement importants peuvent se produire. L'accent doit être mis sur l'implémentation rapide des fonctionnalités souhaitées plutôt que sur leur propreté, puis sur l'optimisation des systèmes là où cela compte vraiment. Le profileur de performances des outils pour les développeurs Chrome est inestimable pour cette étape et m'a aidé à diagnostiquer certains problèmes majeurs avec les versions précédentes du jeu. Votre temps en tant que développeur est précieux. Veillez donc à ne pas passer du temps à réfléchir à des problèmes qui peuvent s'avérer insignifiants ou redondants.
Surveiller l'expérience utilisateur
Lorsque vous implémentez toutes ces astuces, il est important de vous assurer que le jeu fonctionne comme prévu dans le monde réel. L'adaptation à une gamme de fonctionnalités matérielles est un aspect essentiel de tout développement de jeu, mais les jeux Web peuvent cibler un spectre beaucoup plus large, comprenant à la fois des ordinateurs de bureau haut de gamme et des appareils mobiles vieux de dix ans. Le moyen le plus simple d'y parvenir consiste à proposer des paramètres permettant d'adapter les goulots d'étranglement les plus probables de votre codebase, à la fois pour les tâches intensives en GPU et en CPU, comme révélé par votre profileur.
Toutefois, le profilage sur votre propre machine ne peut pas tout couvrir. Il est donc utile de boucler la boucle de rétroaction avec vos utilisateurs d'une manière ou d'une autre. Pour les routes à circulation lente, j'exécute des analyses simples qui fournissent des rapports sur les performances, ainsi que sur des facteurs contextuels tels que la résolution d'écran. Ces données analytiques sont envoyées à un backend Node de base à l'aide de socket.io, ainsi que tous les commentaires écrits que l'utilisateur envoie via le formulaire du jeu. Au début, ces données analytiques ont détecté de nombreux problèmes importants qui pouvaient être atténués par des modifications simples de l'expérience utilisateur, comme mettre en surbrillance le menu des paramètres lorsqu'un nombre de FPS faible est détecté de manière constante ou avertir qu'un utilisateur peut avoir besoin d'activer l'accélération matérielle si les performances sont particulièrement mauvaises.
Les routes lentes à venir
Même après avoir pris toutes ces mesures, une partie importante de la base de joueurs doit jouer avec des paramètres inférieurs, en particulier ceux qui utilisent des appareils légers sans GPU. Bien que la plage de paramètres de qualité disponibles entraîne une distribution des performances assez uniforme, seuls 52% des joueurs atteignent plus de 55 FPS.

Heureusement, il existe encore de nombreuses possibilités d'améliorer les performances. En plus d'ajouter d'autres astuces de rendu pour réduire la demande de GPU, j'espère tester les web workers pour paralléliser la génération d'environnements à court terme. Je pourrais éventuellement avoir besoin d'intégrer WASM ou WebGPU au code source. Tout espace que je pourrais libérer permettra d'obtenir des environnements plus riches et plus diversifiés, ce qui sera l'objectif durable pour le reste du projet.
En tant que projet de loisir, Slow Roads a été un moyen extrêmement satisfaisant de démontrer à quel point les jeux pour navigateur peuvent être étonnamment élaborés, performants et populaires. Si j'ai réussi à piquer votre intérêt pour WebGL, sachez que Slow Roads est un exemple assez superficiel de ses capacités technologiques. Je recommande vivement aux lecteurs d'explorer la présentation de Three.js. Ceux qui s'intéressent au développement de jeux Web en particulier sont invités à rejoindre la communauté sur webgamedev.com.