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

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

Points Clés

Points Clés

Points Clés

  • 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 d'articles sur son blog personnel, garry.net. Rédigés entre 2013 et 2016 — pendant le développement initial mouvementé du jeu et son lancement viral surprise — ces articles couvrent les choix de 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 « Le réseau de Rust » et « Le backend de Rust », complétés par les billets du devblog officiel de Facepunch de la même période.

Indirectement, ils mettent en lumière des décisions auxquelles tout studio multijoueur, des petites équipes indé aux jeux service en ligne en pleine croissance, sera confronté tôt ou tard.

Choisir une bibliothèque réseau (et lui faire confiance aveuglément)

Lorsque Newman construisait la couche réseau de Rust, il s'est tourné vers RakNet. « Raknet est une solution réseau éprouvée et testée », écrivait-il, en soulignant son long historique dans des jeux commercialisés. Oculus l'avait racheté et l'avait rendue open source, ce qui semblait être une bonne nouvelle à l'époque.

Pas tout à fait.

Newman a repéré des bugs dans le dépôt public et a soupçonné que la version open source « avait dû se faire retirer un paquet de trucs » par rapport à la version interne qui avait fait tourner tous ces jeux sortis. Une bibliothèque jouissant d'une solide réputation dans l'industrie était arrivée, dans sa version 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 précise que vous livrez réellement par rapport à votre cas d'usage 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 supplémentaire. Newman lui-même avait évoqué des plans pour remplacer un jour RakNet par Steam Networking Sockets de Valve, un rappel utile que les piles réseau doivent être considérées dès le départ comme des composants remplaçables.

Il convient aussi de noter que cet article a été rédigé en 2016. Le paysage a beaucoup changé depuis. Les développeurs Unity disposent en particulier aujourd'hui d'un large éventail d'options de netcode accessibles et bien maintenues. Pour un aperçu actuel de ce qui existe, le guide d'Edgegap sur les solutions netcode Unity, par exemple, constitue un bon point de départ.

Partager le code entre serveur et client

L'une des décisions architecturales les plus durables dans Rust a consisté à traiter le serveur et le client non pas comme des bases de code séparées, 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 protégée par ces drapeaux de préprocesseur. Newman a emprunté ce schéma au moteur Source de Valve, où il s'était formé en tant que développeur.

Cela évite un point de friction courant : deux bases de code divergentes qui s'éloignent progressivement à mesure que le jeu grandit. Les systèmes partagés restent partagés. Les correctifs se propagent aux deux cibles en même temps. Le modèle mental d'« un jeu, deux vues » correspond parfaitement à la manière dont les systèmes multijoueurs fonctionnent réellement.

Le compromis, c'est la rigueur.

Le code doit être écrit en gardant constamment à l'esprit le contexte dans lequel il s'exécute. Les cas limites exigent de la prudence : en mode serveur en écoute dans l'éditeur, par exemple, un objet physique existe à la fois sous forme serveur et sous forme client au même moment, et il faut empêcher ces deux instances d'interagir entre elles. Ces problèmes ont des solutions. Mais elles ne se résolvent pas toutes seules.

Serveurs en écoute : supprimer les frictions lors des tests

Le flux de travail de développement initial de Rust pour tester le code réseau était, comme l'a dit Newman, « un putain de cauchemar ». Tester la moindre modification nécessitait de compiler une version serveur autonome, de la lancer en arrière-plan, puis de se connecter depuis le client de l'éditeur. C'était tellement profondément ancré dans l'ancienne architecture qu'on ne pouvait pas le modifier facilement, et c'était l'une des principales raisons pour lesquelles l'équipe a choisi de repartir de zéro.

Avec la version reconstruite, la prise en charge du serveur en écoute dans l'éditeur Unity est devenue une priorité absolue dès le premier jour. La solution consistait à définir SERVER et CLIENT simultanément, afin que l'éditeur héberge et rejoigne sa propre partie sans aucun processus externe. Newman tenait à préciser que l'implémentation devait émuler fidèlement un vrai réseau. Les raccourcis qui permettraient au serveur et au client de partager directement des variables masqueraient des bugs qui n'apparaissaient que dans de véritables builds réseau.

Le principe sous-jacent reste valable même si les outils précis ont évolué. Compiler et déployer un serveur dédié pour les tests locaux est aujourd'hui un problème beaucoup plus gérable qu'en 2013. La conteneurisation et les plugins dédiés ont absorbé l'essentiel de la douleur. Le plugin Unity d'Edgegap comprend un outil de build qui réduit la compilation d'un serveur dédié à quelques minutes, même pour de gros projets, et l' Unreal Docker Extension qui permet d'éviter de construire Unreal à partir du code source, réduisant ce qui était autrefois un processus de plusieurs heures ramené à environ 8 minutes. L'objectif est le même que le serveur en écoute de Newman : obtenir un serveur fonctionnel devant un développeur le plus vite 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 l'ensemble de son processus de déploiement à partir de zéro. « Auparavant, déployer de nouvelles versions était lent et truffé de bugs », écrivait Newman. Les builds étaient compilés sur « l'ordinateur perso de quelqu'un », et l'ensemble du processus était « une épreuve ».

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écutaient en parallèle, réduisant les temps de build d'environ 40 minutes à moins de 10. Les ressources étaient réparties en plus de 60 bundles, chacun maintenu sous 5 Mo afin 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 Newman l'expliquait : « 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 mis en place dans l'urgence, sous pression, après un lancement viral que personne n'avait pleinement anticipé.

La description de l'état antérieur par Newman est aussi instructive que celle de l'état final. Les builds étaient compilés sur la machine personnelle d'un développeur, et l'ensemble du processus était une épreuve. Les outils qui l'ont remplacé étaient simples : Jenkins, SVN, S3, des hash CRC. Rien d'exotique. Ce qui a fait la différence, c'est de les avoir en place et de les faire fonctionner automatiquement, afin que l'équipe puisse publier des mises à jour au rythme exigé par la situation.

Élagage PVS : n'envoyer que 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 d'élagage basé sur un Potential Visibility Set (PVS) 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 que les mises à jour d'état de sa propre cellule et de celles qui l'entourent.

Le concept est simple. L'implémentation ne l'est pas. Il faut suivre les objets qui passent d'une cellule à l'autre, envoyer des notifications d'entrée et de sortie à chaque joueur concerné, gérer les joueurs qui chevauchent plusieurs cellules à la fois, et traiter tous les cas limites qui surgissent lorsque des entités sont en mouvement. Comme Newman l'écrivait dans le devblog, « il y a beaucoup d'éléments mobiles ».

Le gain est significatif. Sans culling de visibilité, un serveur qui diffuse l'état complet du monde à chaque joueur connecté atteint vite les limites de bande passante et de CPU. Le PVS maintient ces coûts proportionnels à la densité locale plutôt qu'à la population totale du serveur. C'est ce qui rend les jeux de survie à grand monde viables à grande échelle, et c'est une technique applicable bien au-delà du genre spécifique de Rust. Tout jeu multijoueur avec un vaste monde explorable devrait disposer d'une forme de filtrage de pertinence des états avant de se soucier d'optimisations plus exotiques.

Cet article s'appuie sur les articles de blog originaux de Garry Newman, publiés sur garry.net (https://garry.net/posts/rusts-networking et https://garry.net/posts/the-rust-backend) et sur le devblog officiel de Rust de Facepunch (https://rust.facepunch.com/news/friday-devblog-6).

Tous les droits sur le contenu original appartiennent à leurs propriétaires respectifs.

Écrit par

l'équipe Edgegap

Intégrer Edgegap facilement en quelques minutes

Commencez l'intégration maintenant!

Commencez l'intégration maintenant!