Découvrez comment « Slow Roads » intrigue les gamers et les développeurs à travers les incroyables capacités de la 3D dans le navigateur

Découvrez le potentiel de WebGL dans ce jeu de conduite grand public qui offre des paysages infinis générés de manière procédurale.

Slow Roads est un jeu de conduite grand public qui met l'accent sur des paysages générés sans cesse de manière procédurale, tous hébergés dans le navigateur sous la forme d'une application WebGL. Pour beaucoup, une expérience aussi intensive peut sembler inadaptée dans le contexte limité du navigateur. Le fait de remédier à cette attitude a été l'un de mes objectifs dans ce projet. Dans cet article, je vais vous présenter certaines des techniques que j'ai utilisées pour surmonter cet obstacle et mettre en avant le potentiel souvent négligé de la 3D sur le Web.

Développement 3D dans le navigateur

Après la publication de "Slow Roads", j'ai vu 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é. D'après l'enquête State of JS en 2022, environ 80% des développeurs n'ont pas encore testé WebGL. Il me semble regrettable d'avoir manqué un tel potentiel, en particulier pour les jeux sur navigateur. Avec "Slow Roads", j'espère mettre WebGL sur le devant de la scène 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 ces dernières années, ses écosystèmes de développement sont devenus des outils et des bibliothèques hautement 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 informatique. Three.js, la bibliothèque WebGL majeure, sert de base à de nombreuses extensions, y compris react-three-fiber, qui intègre des composants 3D au framework React. Il existe désormais 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.

Malgré l'utilité remarquable de ces bibliothèques, les projets les plus ambitieux finissent par être soumis à des limites techniques. Les sceptiques à l'égard du jeu basé sur un navigateur pourraient souligner que JavaScript est à thread unique et aux ressources limitées. Mais contourner ces limites libère la valeur cachée: aucune autre plate-forme n'offre la même accessibilité instantanée et la même compatibilité de masse que permet le navigateur. Les utilisateurs de tout système compatible avec les navigateurs peuvent commencer à jouer en un clic, sans avoir à installer d'applications ni à se connecter aux services. Sans oublier que les développeurs apprécient de pouvoir disposer de frameworks d'interface robustes pour créer des interfaces utilisateur ou gérer la mise en réseau pour les modes multijoueurs. Selon moi, ces valeurs font du navigateur une plate-forme si excellente pour les joueurs et les développeurs. Et, comme l'a démontré l'équipe de "Slow Roads", les limites techniques peuvent souvent se résumer à un problème de conception.

Améliorer les performances sur les routes lentes

Étant donné que les éléments essentiels des routes lentes impliquent des mouvements à grande vitesse et la génération de paysages coûteux, le besoin de performances fluides a souligné chaque décision de conception. Ma stratégie principale consistait à commencer par une conception de jeu épurée permettant d'utiliser des raccourcis contextuels dans l'architecture du moteur. L'inconvénient, c'est de renoncer à certaines fonctionnalités pratiques au profit du minimalisme, mais d'obtenir un système sur mesure, hyper-optimisé, qui fonctionne parfaitement sur différents navigateurs et appareils.

Vous trouverez ci-dessous une analyse des principaux composants des routes lentes.

Façonner le moteur d'environnement selon le gameplay

Composant clé du jeu, le moteur de génération d'environnements est inévitablement coûteux et prend à juste titre la plus grande partie des budgets alloués à la mémoire et au calcul. L'astuce utilisée ici consiste à planifier et à répartir les calculs lourds sur une période donnée, afin de ne pas interrompre la fréquence d'images avec des pics de performances.

L'environnement est composé de tuiles géométriques dont la taille et la résolution (classées par "niveaux de détail" ou "niveaux de détail") varient en fonction de leur proximité de la caméra. Dans les jeux classiques équipés d'une caméra en itinérance en libre-service, les différents niveaux d'accès doivent être chargés et déchargés en permanence afin de détailler l'environnement du joueur où qu'il se trouve. Cela peut être une opération coûteuse et inefficace, en particulier lorsque l'environnement lui-même est généré dynamiquement. Heureusement, cette convention peut être entièrement renversée dans les routes lentes en raison de l'attente contextuelle que l'utilisateur doit rester sur la route. À la place, une géométrie très détaillée peut être réservée au couloir étroit qui borde directement l'itinéraire.

Schéma montrant comment générer la route longtemps à l'avance peut permettre une planification proactive et une mise en cache de la génération de l'environnement.
Vue de la géométrie de l'environnement dans les routes lentes sous forme de maquette fonctionnelle indiquant les couloirs géométriques haute résolution bordant la route. Les parties éloignées de l'environnement, qui ne doivent jamais être vues de près, sont affichées dans une résolution beaucoup plus faible.

La ligne médiane de la route est générée bien avant l'arrivée du joueur, ce qui permet de prédire avec précision où et quand les détails de l'environnement seront nécessaires. Le résultat est un système léger qui peut planifier de manière proactive les tâches coûteuses, générant uniquement le minimum nécessaire à chaque moment, sans gaspiller d'efforts sur des détails qui ne seront pas visibles. Cette technique n'est possible que parce que la route est une voie unique sans bifurcation : c'est un bon exemple de compromis dans le jeu tenant compte des raccourcis architecturaux.

Schéma montrant comment générer la route longtemps à l'avance peut permettre une planification proactive et une mise en cache de la génération de l'environnement.
En examinant une certaine distance le long de la route, vous pouvez anticiper les segments environnementaux et les générer progressivement juste avant d'en avoir besoin. De plus, tous les fragments qui seront réexaminés dans un avenir proche peuvent être identifiés et mis en cache pour éviter une nouvelle génération inutile.

Être pointu avec les lois de la physique

Après les besoins en calcul du moteur de l'environnement, il y a la simulation physique. Les routes lentes utilisent un moteur physique personnalisé et minimal qui prend tous les raccourcis disponibles.

Le principal intérêt est d'éviter de simuler un trop grand nombre d'objets en premier lieu. Vous vous penchez sur le contexte minimal et zen en écartant des éléments tels que les collisions dynamiques et les objets destructibles. L'hypothèse selon laquelle le véhicule restera sur la route signifie que les collisions avec des objets tout-terrain peuvent raisonnablement être ignorées. En outre, le codage de la route en tant que ligne médiane creuses permet de détecter rapidement les collisions au niveau de la surface de la route et des garde-fous, tout en se basant sur la vérification de la distance par rapport au centre de la route. La conduite tout-terrain devient alors plus chère, mais il s'agit d'un autre exemple de compromis équitable adapté au contexte du jeu.

Gérer l'espace mémoire utilisé

Il s'agit d'une autre ressource limitée par le navigateur. Il est donc important de gérer la mémoire avec précaution, bien que JavaScript ait été éliminé par la récupération de mémoire. Cela peut être facile à négliger, mais déclarer même de petites quantités de nouvelle mémoire dans une boucle de jeu peut entraîner des problèmes importants avec une fréquence de 60 Hz. En plus de consommer les ressources de l'utilisateur dans un contexte où il est probablement en mode multitâche, les récupérations de mémoire volumineuses peuvent prendre plusieurs frames, ce qui entraîne des saccades visibles. Pour éviter cela, la mémoire en boucle peut être préallouée dans des variables de classe au moment de l'initialisation et recyclée dans chaque image.

Une vue avant/après du profil de mémoire au cours de l'optimisation du codebase Ralenti, indiquant des économies considérables et une réduction du taux de récupération de mémoire.
Bien que l'utilisation globale de la mémoire soit à peine modifiée, la pré-allocation et le recyclage de la mémoire issue de la boucle peuvent considérablement réduire l'impact des récupérations de mémoire coûteuses.

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 majorité de la géométrie se trouve sur une sorte de tapis de course : une fois qu'un vieux morceau prend du retard, ses structures de données peuvent être stockées et recyclées à nouveau pour une partie du monde à venir, un schéma de conception connu sous le nom de pooling d'objets.

Ces pratiques permettent de prioriser l'exécution Lean, avec le sacrifice d'une certaine simplicité de code. Dans les contextes à hautes performances, il est important de faire attention à la facilité d'utilisation que les fonctionnalités 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 incroyablement pratiques, mais il est facile de négliger que chacune crée un tableau pour sa valeur renvoyée. Comprendre le fonctionnement interne de ces boîtes noires peut vous aider à affiner votre code et à éviter les performances instables.

Réduire le temps de chargement avec des assets 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 habituelles concernant le temps de chargement initial des pages Web restent vraies. Les utilisateurs seront plus indulgents lorsqu'ils accèdent sciemment à des contenus lourds, mais des temps de chargement longs peuvent toujours être nuisibles à l'expérience, si ce n'est à fidéliser les utilisateurs. Les jeux nécessitent souvent de grands assets sous la forme de textures, de sons et de modèles 3D. Ils doivent au minimum être soigneusement compressés là où les détails peuvent être épargnés.

Vous pouvez également générer des assets au niveau du client de manière procédurale afin d'éviter les longs transferts. Il s'agit d'un avantage considérable pour les utilisateurs dont la connexion est lente. Cela permet au développeur de contrôler plus directement la composition de son jeu, non seulement pour l'étape de chargement initial, mais aussi pour adapter les niveaux de détails à différents paramètres de qualité.

Comparaison illustrant comment la qualité de la géométrie générée de façon procédurale dans les routes lentes peut être adaptée dynamiquement aux besoins de performances de l'utilisateur.

Dans les routes lentes, la majorité des éléments géométriques sont simplistes et générés de manière procédurale. Les nuanceurs personnalisés combinent plusieurs textures pour faire ressortir les 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 texture stochastique qui permet d'obtenir plus de détails à partir de textures sources de petite taille. À un niveau extrême, il est également possible de générer des textures entièrement sur le client à l'aide d'outils comme texgen.js. Il en va de même pour l'audio : l'API Web Audio permet de générer du son à l'aide de nœuds audio.

Grâce aux assets procéduraux, la création de l'environnement initial ne prend en moyenne que 3,2 secondes. Pour tirer le meilleur parti de la petite taille de téléchargement initiale, un simple écran de démarrage accueille les nouveaux visiteurs et reporte l'initialisation de la scène coûteuse jusqu'à ce que l'utilisateur appuie sur le bouton. Cela constitue également un tampon pratique pour les sessions avec rebond, ce qui réduit le gaspillage du transfert d'éléments chargés dynamiquement.

Histogramme des temps de chargement montrant un pic dans les trois premières secondes (représentant plus de 60% des utilisateurs), suivi d'un déclin rapide. L'histogramme indique que pour plus de 97% des utilisateurs, les temps de chargement sont inférieurs à 10 secondes.

Adopter une approche agile face à l'optimisation tardive

J'ai toujours considéré que le codebase de "Slow Roads" était expérimental et, à ce titre, j'ai adopté une approche extrêmement agile en matière de développement. Lorsque vous travaillez avec une architecture système complexe et en constante évolution, il peut être difficile de prévoir où les goulots d'étranglement importants peuvent se produire. L'accent doit être mis sur la mise en œuvre rapide des fonctionnalités souhaitées, plutôt que de manière proprement dite, puis sur le travail en arrière pour optimiser les systèmes là où cela compte vraiment. Le Profileur de performances disponible dans les outils pour les développeurs Chrome est inestimable pour cette étape. Il m'a aidé à diagnostiquer des problèmes majeurs dans les versions antérieures du jeu. Votre temps en tant que développeur est précieux. Veillez donc à ne pas consacrer de temps à réfléchir à des problèmes qui pourraient s'avérer insignifiants ou redondants.

Surveiller l'expérience utilisateur

Lorsque vous mettez en œuvre tous ces tours, il est important de vous assurer que le jeu fonctionne comme prévu. L'adaptation d'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, incluant à la fois les ordinateurs de bureau haut de gamme et les appareils mobiles vieux de 10 ans. Le moyen le plus simple d'y parvenir est de proposer des paramètres permettant d'adapter les goulots d'étranglement les plus probables dans votre codebase (pour les tâches nécessitant une utilisation intensive du processeur et du GPU) tels qu'ils ont été révélés par votre profileur.

Toutefois, le profilage sur votre propre machine ne peut couvrir que l'étendue de ces besoins. Il est donc important de boucler la boucle de rétroaction avec vos utilisateurs d'une manière ou d'une autre. Pour les routes lentes, j'exécute des analyses simples qui établissent des rapports sur les performances ainsi que sur des facteurs contextuels tels que la résolution d'écran. Ces analyses sont envoyées à un backend de nœud de base à l'aide de socket.io, avec les commentaires écrits envoyés par l'utilisateur via le formulaire intégré au jeu. Au début, ces analyses ont détecté de nombreux problèmes importants qui pouvaient être atténués par de simples modifications de l'expérience utilisateur, telles que la mise en évidence du menu des paramètres lorsqu'un FPS faible est constamment détecté ou un avertissement indiquant qu'un utilisateur pourrait avoir besoin d'activer l'accélération matérielle si les performances sont particulièrement mauvaises.

Les routes lentes sur votre chemin

Même après avoir pris toutes ces mesures, il reste une partie importante de la base de joueurs qui doit jouer avec des paramètres inférieurs, principalement ceux qui utilisent des appareils légers sans GPU. Bien que la gamme de paramètres de qualité disponibles permette d'équilibrer les performances, seuls 52% des joueurs atteignent plus de 55 FPS.

Une matrice définie par le paramètre de distance de visionnage par rapport au paramètre de détail, montrant la moyenne d'images par seconde obtenues avec différentes paires. La répartition est répartie de façon assez équitable entre 45 et 60, 60 étant la cible pour de bonnes performances. Les utilisateurs ayant défini des paramètres bas ont tendance à constater un FPS inférieur à ceux associés à des paramètres élevés, ce qui met en évidence les différences au niveau des capacités matérielles du client.
Notez que ces données sont quelque peu faussées par le nombre d'utilisateurs qui exécutent leur navigateur en désactivant l'accélération matérielle, ce qui entraîne souvent une baisse artificielle des performances.

Heureusement, il existe encore de nombreuses possibilités d'économiser des performances. En plus d'ajouter d'autres astuces de rendu pour réduire la demande de GPU, j'espère tester à court terme des workers Web qui pourront charger la génération d'environnements en parallèle. À terme, il sera peut-être nécessaire d'intégrer WASM ou WebGPU dans le codebase. Toute marge de manœuvre que je pourrai libérer me permettra de créer des environnements plus riches et plus diversifiés, ce qui sera l'objectif à long terme pour le reste du projet.

Dans le cadre de ses projets de loisirs, The Slow Roads s'est révélé être un moyen extrêmement gratifiant de démontrer à quel point les jeux sur navigateur sont étonnamment élaborés, performants et populaires. Si j'ai réussi à susciter votre intérêt pour WebGL, sachez que le protocole de technologie lente Roads est un exemple assez superficiel de ses capacités complètes. J'encourage vivement les lecteurs à explorer la présentation Three.js. Les personnes qui s'intéressent en particulier au développement de jeux Web sont les bienvenues dans la communauté sur webgamedev.com.