Sur le modèle réseau dans les jeux pour débutants

Sur le modèle réseau dans les jeux pour débutants
Au cours des deux dernières semaines, j'ai travaillé sur un moteur réseau pour mon jeu. Avant cela, je ne savais rien des technologies réseau dans les jeux, donc j'ai lu de nombreux articles et effectué de nombreuses expériences pour comprendre tous les concepts et être capable d'écrire mon propre moteur réseau.

Dans ce guide, je voudrais partager avec vous les différentes concepts que vous devez étudier avant d'écrire votre propre moteur de jeu, ainsi que les meilleures ressources et articles pour les apprendre.

En général, il existe deux types principaux d'architectures réseau : peer-to-peer et client-serveur. Dans l'architecture peer-to-peer (p2p), les données sont échangées entre n'importe quelle paire de joueurs connectés, tandis que dans l'architecture client-serveur, les données ne sont échangées qu'entre les joueurs et le serveur.

Bien que l'architecture peer-to-peer soit toujours utilisée dans certains jeux, le standard est l'architecture client-serveur : elle est plus simple à mettre en œuvre, nécessite moins de bande passante et facilite la protection contre la triche. C'est pourquoi dans ce guide, nous nous concentrerons sur l'architecture client-serveur.

En particulier, nous nous intéressons le plus aux serveurs autoritaires : dans ces systèmes, le serveur a toujours raison. Par exemple, si un joueur pense qu'il est à la position (10, 5), et que le serveur lui dit qu'il est à (5, 3), alors le client doit remplacer sa position par celle transmise par le serveur, et non l'inverse. Utiliser des serveurs autoritaires simplifie la détection des tricheurs.

Dans les systèmes réseau de jeux, il y a trois composants principaux :

  • Protocole de transport : comment les données sont transférées entre les clients et le serveur.
  • Protocole d'application : ce qui est transmis des clients au serveur et du serveur aux clients, et sous quel format.
  • Logique d'application : comment les données transférées sont utilisées pour mettre à jour l'état des clients et du serveur.

Il est très important de comprendre le rôle de chaque partie et les difficultés associées.

Protocole de transport

La première étape consiste à choisir un protocole pour le transport des données entre le serveur et les clients. Pour cela, il existe deux protocoles Internet : TCP et UDP. Mais vous pouvez également créer votre propre protocole de transport basé sur l'un d'eux ou utiliser une bibliothèque qui les utilise.

Comparaison TCP et UDP

Tant TCP qu'UDP sont basés sur IP. Un protocole IP permet de transmettre un paquet de la source au destinataire, mais ne garantit pas que le paquet envoyé arrivera un jour ou l'autre au destinataire, ni qu'il le recevra au moins une fois et que la séquence des paquets arrivera dans le bon ordre. De plus, un paquet peut contenir uniquement une taille de données limitée, déterminée par la taille MTU.

. UDP n'est qu'une fine couche au-dessus de l'IP. Par conséquent, il a les mêmes limitations. En revanche, le TCP possède de nombreuses caractéristiques. Il assure une connexion fiable et ordonnée entre deux nœuds avec vérification des erreurs. Ainsi, le TCP est très pratique et utilisé dans de nombreux autres protocoles, par exemple, dans . L'augmentation de la performance de Nginx est due à l'utilisation d'une architecture asynchrone gérée par événements, contrairement à un modèle multithread. Cela, combiné à un haut niveau de parallélisme, permet à Nginx de traiter les requêtes avec une mémoire minimale. Cela fait de Nginx une excellente solution pour les serveurs web, qu'il s'agisse de gros ou de petits volumes de trafic. Nginx est une solution mature et entièrement documentée, permettant une configuration facile selon vos besoins. Les développeurs utilisent également Nginx comme proxy inverse pour, FTP et SMTP. Mais toutes ces fonctions ont un coût : un délai.

Pour comprendre pourquoi ces fonctionnalités peuvent provoquer des retards, il faut examiner comment fonctionne le TCP. Lorsque le nœud expéditeur transmet un paquet au nœud destinataire, il s'attend à recevoir une confirmation (ACK). Si, après un certain temps, il ne la reçoit pas (parce que le paquet ou la confirmation a été perdu, ou pour d'autres raisons), il renvoie le paquet. De plus, le TCP garantit la réception des paquets dans le bon ordre, donc tant que le paquet perdu n'est pas reçu, tous les autres paquets ne peuvent pas être traités, même s'ils ont déjà été reçus par le nœud destinataire.

Mais comme vous pouvez l'imaginer, le délai dans les jeux multijoueurs est très important, surtout dans des genres aussi dynamiques que les FPS. C'est pourquoi de nombreux jeux utilisent l'UDP avec leur propre protocole.

Un protocole propriétaire basé sur l'UDP peut être plus efficace que le TCP pour diverses raisons. Par exemple, il peut marquer certains paquets comme fiables et d'autres comme non fiables. Ainsi, il ne se soucie pas de savoir si un paquet non fiable a atteint le destinataire. Ou il peut gérer plusieurs flux de données, de sorte qu'un paquet perdu dans un flux ne ralentisse pas les autres flux. Par exemple, il peut y avoir un flux pour les entrées du joueur et un autre pour les messages de chat. Si un message de chat qui n'est pas une donnée urgente est perdu, cela ne ralentira pas le déclenchement des entrées, qui sont urgentes. Alternativement, un protocole propriétaire peut implémenter la fiabilité différemment de celle du TCP pour être plus efficace dans les conditions des jeux vidéo.

Donc, si TCP est si mauvais, allons-nous créer notre propre protocole de transport basé sur UDP ?

C'est un peu plus compliqué. Même si TCP est presque suboptimal pour les systèmes de jeu en réseau, il peut très bien fonctionner spécifiquement dans votre jeu et vous faire économiser un temps précieux. Par exemple, la latence peut ne pas être un problème pour un jeu au tour par tour ou un jeu qui peut être joué uniquement sur des réseaux LAN, où les latences et la perte de paquets sont bien inférieures à celles d'Internet.

De nombreux jeux à succès, dont World of Warcraft, Minecraft et Terraria, utilisent TCP. Cependant, dans la plupart des FPS, des protocoles personnalisés basés sur UDP sont utilisés, c'est pourquoi nous allons en parler plus en détail ci-dessous.

Si vous décidez d'utiliser TCP, assurez-vous que l'algorithme de Nagle, car il met en tampon les paquets avant l'envoi, ce qui augmente donc la latence.

Pour en savoir plus sur les différences entre UDP et TCP dans le contexte des jeux multijoueurs, vous pouvez lire l'article de Glenn Fiedler UDP vs. TCP.

Protocole personnalisé

Donc, vous voulez créer votre propre protocole de transport, mais vous ne savez pas par où commencer ? Vous avez de la chance, car Glenn Fiedler a écrit deux articles incroyables à ce sujet. Vous y trouverez de nombreuses réflexions intéressantes.

Le premier article, Networking for Game Programmers de 2008, est plus simple que le second, Building A Game Network Protocol de 2016. Je vous recommande de commencer par le plus ancien.

Gardez à l'esprit que Glenn Fiedler est un grand partisan de l'utilisation de protocoles personnalisés basés sur UDP. Et après avoir lu ses articles, vous adopterez probablement son avis selon lequel TCP a de sérieux inconvénients dans les jeux vidéo et chercherez à mettre en œuvre votre propre protocole.

Mais si vous êtes novice en matière de réseaux, faites-vous une faveur et utilisez TCP ou une bibliothèque. Pour réaliser avec succès votre propre protocole de transport, vous devrez d’abord apprendre beaucoup.

Bibliothèques réseau

Si vous avez besoin de quelque chose de plus efficace que TCP, mais que vous ne voulez pas vous embêter à implémenter votre propre protocole et à vous plonger dans de nombreux détails, vous pouvez utiliser une bibliothèque réseau. Il en existe beaucoup :

Je n'ai pas tout essayé, mais je préfère ENet car il est facile à utiliser et fiable. De plus, il dispose d'une documentation claire et d'un tutoriel pour les débutants.

Protocole de transport : conclusion

En résumé : il existe deux principaux protocoles de transport : TCP et UDP. TCP possède de nombreuses fonctionnalités utiles : fiabilité, maintien de l'ordre des paquets, détection des erreurs. UDP n'a pas tout cela, mais TCP a naturellement des latences accrues, inacceptables pour certains jeux. Ainsi, pour garantir des latences faibles, on peut créer un protocole personnalisé basé sur UDP ou utiliser une bibliothèque implémentant un protocole de transport sur UDP et adapté aux jeux vidéo multijoueurs.

Le choix entre TCP, UDP et une bibliothèque dépend de plusieurs facteurs. Tout d'abord, des besoins du jeu : a-t-il besoin de faibles latences ? Deuxièmement, des exigences du protocole de l'application : a-t-il besoin d'un protocole fiable ? Comme nous le verrons dans la partie suivante, il est possible de créer un protocole d'application qui peut tout à fait fonctionner avec un protocole non fiable. Enfin, il faut aussi prendre en compte l'expérience du développeur du moteur réseau.

J'ai deux conseils :

  • Abstraire au maximum le protocole de transport du reste de l'application, afin qu'il puisse être facilement remplacé sans avoir à réécrire tout le code.
  • Ne vous lancez pas dans une optimisation prématurée. Si vous n'êtes pas un expert en réseaux et que vous n'êtes pas sûr d'avoir besoin de votre propre protocole de transport basé sur UDP, vous pouvez commencer avec TCP ou une bibliothèque offrant fiabilité, puis tester et mesurer les performances. Si des problèmes surviennent et que vous êtes sûr que la cause réside dans le protocole de transport, il pourrait être temps de créer votre propre protocole de transport.

Pour conclure cette partie, je vous recommande de lire Introduction to Multiplayer Game Programming de Bryan Hook, qui aborde de nombreux sujets discutés ici.

Protocole d'application

Maintenant que nous pouvons échanger des données entre les clients et le serveur, il faut décider quelles données transmettre et sous quel format.

Le schéma classique consiste à ce que les clients envoient au serveur des entrées ou des actions, tandis que le serveur envoie aux clients l'état actuel du jeu.

Le serveur envoie un état filtré et non complet avec les entités situées à proximité du joueur. Il le fait pour trois raisons. Premièrement, l'état complet peut être trop volumineux pour être transmis à une fréquence élevée. Deuxièmement, les clients s'intéressent principalement aux données visuelles et audio, car la majorité de la logique de jeu est simulée sur le serveur de jeu. Troisièmement, dans certains jeux, le joueur ne doit pas connaître certaines données, par exemple, la position de l'adversaire à l'autre bout de la carte, sinon il pourrait analyser les paquets et savoir exactement où aller pour l'éliminer.

Sérialisation

La première étape consiste à transformer les données que nous souhaitons envoyer (entrée ou état du jeu) dans un format approprié pour la transmission. Ce processus s'appelle sérialisation.

Il peut être tentant d'utiliser un format lisible par l'homme, comme JSON ou XML. Cependant, cela serait totalement inefficace et occuperait une grande partie de la bande passante.

Au lieu de cela, il est recommandé d'utiliser un format binaire, qui est beaucoup plus compact. Cela signifie que les paquets ne contiendront que quelques octets. Il faut prendre en compte le problème de l'ordre des octets, qui peut différer sur différents ordinateurs.

Pour sérialiser les données, vous pouvez utiliser une bibliothèque, par exemple :

Assurez-vous simplement que la bibliothèque produit des archives portables et prend en compte l'ordre des octets.

Une solution alternative peut être une implémentation autonome, qui n'est pas particulièrement complexe, surtout si vous utilisez une approche axée sur les données dans le code. De plus, cela vous permettra d'effectuer des optimisations qui ne sont pas toujours possibles lors de l'utilisation d'une bibliothèque.

Glenn Fiedler a écrit deux articles sur la sérialisation : Lire et écrire des paquets et Stratégies de sérialisation.

Compression

La quantité de données transférées entre les clients et le serveur est limitée par la bande passante du canal. La compression des données permettra de transmettre plus de données dans chaque instantané, d'augmenter la fréquence de mise à jour ou simplement de réduire les exigences sur le canal.

Emballage de bits

La première technique est l'emballage binaire. Cela consiste à utiliser exactement le nombre de bits nécessaire pour décrire la quantité requise. Par exemple, si vous avez une énumération qui peut avoir 16 valeurs différentes, vous pouvez utiliser seulement 4 bits au lieu d'un octet entier (8 bits).

Glenn Fiedler explique comment le mettre en œuvre dans la deuxième partie de l'article. Lire et écrire des paquets.

L'emballage binaire fonctionne particulièrement bien avec la discrétisation, qui sera le sujet de la prochaine section.

Discrétisation

Discrétisation est une technique de compression avec perte qui consiste à utiliser seulement un sous-ensemble des valeurs possibles pour coder une quantité. La manière la plus simple de mettre en œuvre la discrétisation est l'arrondi des nombres à virgule flottante.

Glenn Fiedler (encore une fois !) montre comment appliquer la discrétisation en pratique dans son article. Compression par instantané.

Les algorithmes de compression

La prochaine technique concernera les algorithmes de compression sans perte.

Voici, à mon avis, trois algorithmes particulièrement intéressants à connaître :

  • Codage de Huffman avec un code pré-calculé, qui est extrêmement rapide et peut donner de bons résultats. Il a été utilisé pour compresser des paquets dans le moteur réseau Quake3.
  • zlib est un algorithme de compression général qui n'augmente jamais la taille des données. Comme on peut le voir ici, il a été appliqué dans de nombreux domaines. Pour la mise à jour des états, il peut s'avérer redondant. Mais il peut aussi être utile si vous devez envoyer des ressources depuis le serveur aux clients, comme des actifs, de longs textes ou des reliefs.
  • La copie de longues séries est probablement l'algorithme de compression le plus simple, mais il est très efficace pour certains types de données et peut être utilisé comme étape de prétraitement avant zlib. Il est particulièrement adapté pour la compression des reliefs constitués de tuiles ou de voxels, où de nombreux éléments voisins se répètent.

Compression delta

La dernière méthode de compression est la compression delta. Elle consiste à ne transmettre que les différences entre l'état de jeu actuel et le dernier état reçu par le client.

Elle a été appliquée pour la première fois dans le moteur réseau Quake3. Voici deux articles expliquant comment l'utiliser :

Glenn Fiddler a également utilisé cela dans la deuxième partie de son article. Compression par instantané.

Chiffrement

De plus, vous pouvez avoir besoin de chiffrer les transmissions d'informations entre les clients et le serveur. Il existe plusieurs raisons à cela :

  • la confidentialité : les messages peuvent être lus uniquement par le destinataire, et aucune autre personne effectuant une écoute de réseau ne pourra les lire.
  • l'authentification : la personne souhaitant jouer le rôle du joueur doit connaître sa clé.
  • prévention de la triche : les joueurs malveillants auront beaucoup plus de mal à créer leurs propres paquets pour tricher, ils devront reproduire le schéma de chiffrement et trouver la clé (qui change à chaque connexion).

Je recommande vivement d'utiliser une bibliothèque pour cela. Je suggère d'utiliser libsodium, car elle est particulièrement simple et dispose d'excellents tutoriels. Le tutoriel sur l'échange de clés, qui permet de générer de nouvelles clés à chaque nouvelle connexion, est particulièrement intéressant.

Protocole de l'application : conclusion

Nous allons maintenant conclure sur le protocole de l'application. Je pense que la compression est tout à fait facultative et que la décision de l'utiliser dépend uniquement du jeu et de la bande passante requise. Le chiffrement, à mon avis, est indispensable, mais il est possible de s'en passer dans le premier prototype.

Logique de l'application

Nous sommes maintenant capables de mettre à jour l'état du client, mais nous pourrions rencontrer des problèmes de latence. Après avoir effectué une entrée, le joueur doit attendre la mise à jour de l'état du jeu de la part du serveur pour voir quel impact il a eu sur le monde.

De plus, entre deux mises à jour de l'état, le monde est complètement statique. Si la fréquence des mises à jour est basse, les mouvements seront très saccadés.

Il existe plusieurs techniques permettant de réduire l'impact de ce problème, et je vais en parler dans la section suivante.

Techniques d'atténuation de la latence

Toutes les techniques décrites dans cette section sont examinées en détail dans la série Fast-Paced Multiplayer de Gabriel Gambetta. Je recommande vivement de lire cette merveilleuse série d'articles. Elle comprend également une démo interactive, permettant de voir comment ces techniques fonctionnent en pratique.

La première technique consiste à appliquer le résultat de l'entrée directement, sans attendre de réponse du serveur. Cela s'appelle la prédiction côté client.. Cependant, lorsque le client reçoit une mise à jour du serveur, il doit s'assurer que sa prévision était correcte. Si ce n'est pas le cas, il doit simplement ajuster son état en fonction de celle reçue du serveur, car le serveur est autoritaire. Cette technique a été utilisée pour la première fois dans Quake. Pour en savoir plus, vous pouvez lire l'article. Revue de code du moteur Quake de Fabien Sanglard [traduction sur Habr].

Un deuxième ensemble de techniques est utilisé pour lisser le mouvement d'autres entités entre deux mises à jour d'état. Il existe deux manières de résoudre cette tâche : l'interpolation et l'extrapolation. Dans le cas de l'interpolation, on prend les deux derniers états et on montre la transition d'un à l'autre. Son inconvénient est qu'elle entraîne un léger retard, car le client voit toujours ce qui s'est passé dans le passé. L'extrapolation consiste à prédire où les entités devraient se trouver en se basant sur le dernier état reçu par le client. Son inconvénient est que si une entité change complètement de direction, il y aura une grande erreur entre la prévision et la position réelle.

La dernière technique, la plus avancée, utile uniquement dans les FPS — est la compensation de latence. Lors de l'utilisation de la compensation de latence, le serveur prend en compte les délais du client quand il tire sur une cible. Par exemple, si un joueur a effectué un tir à la tête sur son écran, mais qu'en réalité, sa cible était à un autre endroit à cause du délai, il serait injuste de refuser au joueur le droit de tuer à cause de ce même retard. Ainsi, le serveur rembobine le temps au moment où le joueur a tiré, afin de simuler ce que le joueur voyait sur son écran, et de vérifier la collision entre son tir et la cible.

Glenn Fiedler (comme toujours !) a écrit en 2004 un article Physique Réseau (2004), dans lequel il a posé les fondations de la synchronisation de la simulation physique entre le serveur et le client. En 2014, il a écrit une nouvelle série d'articles Physique Réseau, où il a décrit d'autres techniques pour synchroniser la simulation physique.

Il y a aussi deux articles dans le wiki de la société Valve, Réseautage Multijoueur Source et Méthodes de Compensation de Latence dans la Conception et l'Optimisation des Protocoles Client/Serveur dans le Jeu , qui traitent de la compensation des délais.

Prévention de la tricherie

Il existe deux techniques principales pour prévenir la tricherie.

Première : complexité de l'envoi de paquets malveillants par des tricheurs. Comme mentionné précédemment, un bon moyen de le réaliser est le chiffrement.

Deuxième : le serveur autoritaire ne doit recevoir que des commandes / entrées / actions. Le client ne doit pas avoir la possibilité de modifier l'état du serveur, sauf en envoyant des entrées. Ainsi, le serveur doit vérifier la validité de chaque entrée reçue avant de l'appliquer.

Logique de l'application : conclusion

Je vous recommande de mettre en place une méthode de simulation de grandes latences et de faibles fréquences de mise à jour, afin de pouvoir tester le comportement de votre jeu dans de mauvaises conditions, même lorsque le client et le serveur sont exécutés sur le même ordinateur. Cela simplifiera considérablement la mise en œuvre des méthodes de lissage des latences.

Autres ressources utiles

Si vous souhaitez explorer d'autres ressources consacrées aux modèles réseaux, vous pouvez les trouver ici :

  • Blog de Glenn Fiedler — il vaut la peine de lire l'intégralité de son blog, qui contient de nombreux excellents articles. Ici tous les articles sur les technologies réseau sont rassemblés.
  • Awesome Game Networking de M. Fatih MAR — c'est une liste détaillée d'articles et de vidéos sur les moteurs réseau des jeux vidéo.
  • Dans wiki du subreddit r/gamedev il y a aussi de nombreux liens utiles.

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster