
RUST - Plongée profonde dans le backend de jeu multijoueur

Vérifiez votre middleware réseau : L’adoption de RakNet par Rust a montré qu’un fork open source d’une bibliothèque éprouvée peut contenir des bogues cachés. Validez la version précise que vous livrez, pas seulement la réputation de la bibliothèque.
Une base de code, deux cibles : Facepunch a compilé le serveur et le client à partir d’une base de code partagée unique à l’aide d’indicateurs de préprocesseur, éliminant les divergences entre les deux et réduisant les coûts de maintenance à long terme.
Investissez tôt dans les tests dans l’éditeur : Créer un mode serveur écoutant pour l’éditeur Unity a supprimé un flux de travail de test pénible en plusieurs étapes et a rendu l’itération du code réseau beaucoup plus rapide — un principe qui s’applique à tout projet multijoueur.
L’infrastructure de déploiement n’est pas facultative : Lorsque Rust est devenu viral, l’équipe a dû reconstruire toute sa chaîne de compilation sous la pression ; cette expérience plaide directement pour l’automatisation des déploiements avant le lancement, et non après.
Le culling PVS maintient l’évolutivité des grands mondes : Le système de visibilité de Rust basé sur une grille garantit que les joueurs ne reçoivent des mises à jour d’état que pour les cellules proches, maintenant les coûts de bande passante et de CPU proportionnels à la densité de population locale plutôt qu’à la taille totale du serveur.
Garry Newman, fondateur de Facepunch Studios, a documenté l'architecture réseau et backend de Rust dans une série de billets sur son blog personnel sur garry.net. Écrits entre 2013 et 2016 — pendant le développement initial turbulent du jeu et son lancement viral surprise — ces billets couvrent les choix de la pile réseau, l'architecture du code serveur/client, les flux de travail de test dans l'éditeur, et le pipeline de déploiement qui a dû être reconstruit presque du jour au lendemain. Les deux articles les plus pertinents sont « Rust's Networking » et « The Rust backend », complétés par les entrées officielles du devblog de Facepunch de la même période.
Indirectement, ils mettent en lumière des décisions auxquelles tout studio de jeux multijoueurs, des petites équipes indépendantes aux jeux de service en direct en pleine croissance, sera confronté tôt ou tard.
Choisir une bibliothèque réseau (et s'y fier aveuglément)
Lorsque Newman a construit la couche réseau de Rust, il s'est tourné vers RakNet. « Raknet est une solution réseau éprouvée », a-t-il écrit, soulignant sa longue feuille de route sur de nombreux titres commercialisés. Oculus l'avait acquise et rendue open source, ce qui semblait être une bonne nouvelle à l'époque.
Ce ne l'était pas tout à fait.
Newman a remarqué des bugs dans le dépôt public et a suspecté que la version open-source « avait dû être dépouillée de tout un tas de trucs » par rapport à la version interne qui avait propulsé tous ces jeux commercialisés. Une bibliothèque jouissant d'une solide réputation dans l'industrie était arrivée sous sa forme communautaire dans un état incertain.
La leçon pratique n'est pas d'éviter les bibliothèques réseau tierces. C'est de valider la version spécifique que vous déployez réellement par rapport à votre cas d'utilisation réel, plutôt que de vous fier à la réputation historique de la bibliothèque.
Les forks open-source d'outils commerciaux méritent un examen approfondi. Newman lui-même a mentionné son intention de remplacer à terme RakNet par les Steam Networking Sockets de Valve, un rappel utile que les piles réseau doivent être traitées dès le départ comme des composants remplaçables.
Il convient également de noter que cet article a été écrit en 2016. Le paysage a considérablement changé depuis. Les développeurs Unity en particulier disposent désormais d'un large éventail d'options de netcode accessibles et bien entretenues. Pour un aperçu actuel de ce qui est disponible, le guide d'Edgegap sur les solutions de netcode Unity est, par exemple, un point de départ utile.
Partager le code entre le serveur et le client
L'une des décisions architecturales les plus durables dans Rust a été de traiter le serveur et le client non pas comme des bases de code distinctes, mais comme deux cibles de compilation du même code. La base de code est compilée avec SERVER ou CLIENT défini, et la logique spécifique à un côté est isolée avec ces drapeaux de préprocesseur. Newman a emprunté ce modèle au moteur Source de Valve, au sein duquel il s'était formé en tant que développeur.
Cela permet de contourner un problème courant : deux bases de code divergentes qui s'éloignent progressivement l'une de l'autre à mesure que le jeu grandit. Les systèmes partagés restent partagés. Les corrections de bugs se propagent aux deux cibles à la fois. Le modèle mental d'« un jeu, deux vues » correspond parfaitement au fonctionnement réel des systèmes multijoueurs.
Le compromis est la discipline.
Le code doit être écrit en ayant constamment conscience du contexte dans lequel il s'exécute. Les cas limites nécessitent de la prudence : en mode serveur d'écoute (listen server) de l'éditeur, par exemple, un objet physique existe à la fois sous forme de serveur et de client en même temps, et il faut les empêcher d'interagir l'un avec l'autre. Ces problèmes ont des solutions. Mais ils ne se résolvent pas d'eux-mêmes.
Serveurs d'écoute : Éliminer les frictions lors des tests
Le flux de travail initial du développement de Rust pour tester le code réseau était, comme le disait Newman, « un cauchemar absolu ». Tester le moindre changement nécessitait de compiler une version serveur autonome, de la lancer en arrière-plan, puis de s'y connecter depuis le client de l'éditeur. C'était si profondément ancré dans l'ancienne architecture que cela ne pouvait pas être facilement modifié, et c'est l'une des raisons principales pour lesquelles l'équipe a choisi de recommencer le jeu à zéro.
Avec la version reconstruite, la prise en charge des serveurs d'écoute dans l'éditeur Unity est devenue une priorité absolue dès le premier jour. La solution a consisté à définir simultanément SERVER et CLIENT, de sorte que l'éditeur héberge et rejoigne son propre jeu sans aucun processus externe. Newman a pris soin de noter que l'implémentation devait émuler fidèlement un réseau réel. Les raccourcis permettant au serveur et au client de partager directement des variables masqueraient des bugs qui ne feraient surface que dans les versions réseau réelles.
Le principe sous-jacent reste tout à fait valable même si les outils spécifiques ont évolué. Compiler et déployer un serveur dédié pour des tests locaux est un problème beaucoup plus facile à gérer aujourd'hui qu'en 2013. La conteneurisation et les plugins spécialisés ont éliminé la majeure partie de la complexité. Le plugin Unity d'Edgegap comprend un outil de construction qui réduit la compilation d'un serveur dédié à quelques minutes, même pour les grands projets, et l'extension Unreal Docker permet d'éviter complètement de compiler Unreal depuis les sources, réduisant ce qui était auparavant un processus de plusieurs heures à environ 8 minutes. L'objectif est le même que celui du serveur d'écoute de Newman : mettre un serveur fonctionnel à la disposition d'un développeur le plus rapidement possible, afin que les bugs de netcode apparaissent pendant l'itération plutôt qu'après le lancement.
Construire un pipeline de déploiement avant d'en avoir besoin
Après que Rust est devenu viral fin 2013, l'équipe a passé une semaine à reconstruire entièrement son processus de déploiement. « Auparavant, le déploiement de nouvelles versions était lent et truffé de bugs », écrivait Newman. Les builds étaient compilées sur « l'ordinateur de l'un des gars », tout le processus étant « un calvaire ».
Le système de remplacement utilisait Jenkins CI/CD, déclenché automatiquement par les commits SVN. Des tâches distinctes pour le serveur et le client s'exécututaient en parallèle, réduisant les temps de build d'environ 40 minutes à moins de 10. Les assets ont été divisés en plus de 60 bundles, chacun maintenu sous les 5 Mo pour que les navigateurs puissent les mettre en cache comme des images. Chaque bundle était nommé à l'aide d'un hash CRC, de sorte qu'un fichier ne déclenchait un nouveau téléchargement que lorsque son contenu avait réellement changé, et non à chaque déploiement. Comme l'expliquait Newman, « Nous ne voulons pas que le joueur doive télécharger 300 Mo à chaque fois. »
Le système a fonctionné. Mais il a dû être construit dans l'urgence, sous pression, après un lancement viral que personne n'avait pleinement anticipé.
La description que fait Newman de la situation initiale est aussi instructive que celle de l'étape suivante. Les builds étaient compilées sur la machine personnelle d'un seul développeur, tout le processus étant un calvaire. Les outils qui l'ont remplacé étaient simples : Jenkins, SVN, S3, les hashs CRC. Rien d'exotique. Ce qui a fait la différence, c'est de les avoir en place et de les exécuter automatiquement, afin que l'équipe puisse publier des mises à jour au rythme exigé par la situation.
Culling PVS : Envoyer uniquement ce que les joueurs peuvent voir
L'une des entrées les plus instructives sur le plan technique dans les premiers devblogs de Facepunch décrivait l'introduction d'un système de culling PVS (Potential Visibility Set) dans le netcode de Rust. L'approche est basée sur une grille : le monde est divisé en cellules, et chaque joueur ne reçoit les mises à jour d'état que pour sa propre cellule et celles qui l'entourent.
Le concept est simple. L'implémentation ne l'est pas. Vous devez suivre le déplacement des objets d'une cellule à l'autre, envoyer des notifications d'entrée et de sortie à chaque joueur concerné, gérer les joueurs à cheval sur plusieurs cellules à la fois, et gérer tous les cas limites qui surviennent lorsque les entités sont en mouvement. Comme l'écrivait Newman dans le devblog, « il y a beaucoup de pièces mobiles. »
Le bénéfice est considérable. Sans culling de visibilité, un serveur diffusant l'état complet du monde à chaque joueur connecté atteint rapidement ses limites de bande passante et de processeur. Le PVS maintient ces coûts proportionnels à la densité locale plutôt qu'à la population totale du serveur. C'est ce qui rend viables à grande échelle les jeux de survie en monde ouvert, et c'est une technique applicable bien au-delà du genre spécifique de Rust. Tout jeu multijoueur doté d'un vaste monde explorable devrait disposer d'une forme de filtrage de pertinence de l'état avant de se soucier d'optimisations plus complexes.
—
Cet article est basé sur et cite les articles de blog originaux de Garry Newman, publiés sur garry.net (lien1 et lien2) et le devblog officiel de Rust par Facepunch.
Tous les droits sur le contenu original appartiennent à leurs propriétaires respectifs.
Écrit par
l'équipe Edgegap









