À propos du modèle de réseau dans les jeux pour débutants

image

Depuis deux semaines, je travaille sur un moteur réseau pour mon jeu. Avant cela, je ne connaissais rien aux technologies réseau dans les jeux, j'ai donc lu de nombreux articles et mené de nombreuses expériences pour comprendre tous les concepts et pouvoir écrire mon propre moteur réseau.

Dans ce guide, je voudrais partager avec vous divers concepts que vous devez apprendre 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 principaux types d'architectures de réseau: peer-to-peer et client-serveur. Dans l'architecture peer-to-peer (p2p), les données sont transférées entre n'importe quelle paire de joueurs connectés, et dans l'architecture client-serveur, les données sont transmises uniquement entre les joueurs et le serveur.

Bien que l'architecture peer-to-peer soit encore utilisée dans certains jeux, la norme est client-serveur: elle est plus facile à implémenter, nécessite une largeur de canal plus petite et facilite la protection contre la triche. Par conséquent, dans ce guide, nous nous concentrerons sur l'architecture client-serveur.

En particulier, nous nous intéressons surtout aux serveurs autoritaires: dans de tels systèmes, le serveur a toujours raison. Par exemple, si un joueur pense qu'il est en coordonnées (10, 5) et que le serveur lui dit qu'il est en (5, 3), alors le client doit remplacer sa position par celle transmise par le serveur, et non l'inverse. L'utilisation de serveurs autoritaires facilite la reconnaissance des tricheurs.

Les systèmes de réseaux de jeux comportent 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 transféré des clients au serveur et du serveur aux clients et dans quel format.
  • Logique d'application: comment les données transmises 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 pièce et les difficultés qui y sont associées.

Protocole de transport


La première étape consiste à choisir un protocole de transport de données entre le serveur et les clients. Il existe deux protocoles Internet pour cela: TCP et UDP . Mais vous pouvez créer votre propre protocole de transport basé sur l'un d'eux ou utiliser la bibliothèque dans laquelle ils sont utilisés.

Comparaison de TCP et UDP


TCP et UDP sont tous deux basés sur IP . IP vous permet de transférer un paquet de la source au destinataire, mais ne garantit pas que le paquet envoyé atteindra tôt ou tard le destinataire, qu'il l'atteindra au moins une fois et que la séquence de paquets arrivera dans le bon ordre. De plus, un paquet ne peut contenir qu'une taille de données limitée spécifiée par la valeur MTU .

UDP n'est qu'une fine couche sur IP. Par conséquent, il a les mêmes limitations. En revanche, TCP possède de nombreuses fonctionnalités. Il fournit une connexion ordonnée fiable entre deux nœuds avec vérification des erreurs. Par conséquent, TCP est très pratique et est utilisé dans de nombreux autres protocoles, par exemple en HTTP , FTP et SMTP . Mais toutes ces fonctionnalités ont un prix: le retard .

Pour comprendre pourquoi ces fonctions peuvent provoquer un retard, vous devez comprendre le fonctionnement de TCP. Lorsque le nœud émetteur transmet le paquet au nœud récepteur, il s'attend à recevoir un accusé de réception (ACK). Si après un certain temps, il ne le reçoit pas (parce que le paquet ou la confirmation a été perdu, ou pour d'autres raisons), il renvoie le paquet. De plus, TCP garantit que les paquets sont reçus dans le bon ordre. Par conséquent, jusqu'à ce qu'un paquet perdu soit 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 récepteur.

Mais comme vous le savez probablement, le retard dans les jeux multijoueurs est très important, en particulier dans des genres actifs tels que FPS. C'est pourquoi de nombreux jeux utilisent UDP avec leur propre protocole.

Un protocole natif basé sur UDP peut être plus efficace que TCP pour diverses raisons. Par exemple, il peut marquer certains packages comme approuvés et d'autres comme non approuvés. Par conséquent, il ne se soucie pas si le paquet non approuvé a atteint le récepteur. Ou il peut traiter plusieurs flux de données afin qu'un paquet perdu dans un flux ne ralentisse pas les flux restants. Par exemple, il peut y avoir un flux pour l'entrée du lecteur et un autre flux pour les messages de discussion. Si un message de chat qui n'est pas des données urgentes est perdu, cela ne ralentira pas l'entrée, qui est urgente. Ou, un protocole propriétaire peut implémenter la fiabilité différemment de TCP afin d'être plus efficace dans les jeux vidéo.

Donc, si TCP est tellement nul, alors nous allons créer notre propre protocole de transport basé sur UDP?

Tout est un peu plus compliqué. Même si TCP est presque sous-optimal pour les systèmes de jeu en réseau, il peut très bien fonctionner dans votre jeu et vous faire gagner un temps précieux. Par exemple, le retard peut ne pas être un problème pour un jeu au tour par tour ou un jeu qui ne peut être joué que sur des réseaux locaux, où il y a beaucoup moins de retards et de pertes de paquets que sur Internet.

De nombreux jeux à succès, notamment World of Warcraft, Minecraft et Terraria, utilisent TCP. Cependant, la plupart des FPS utilisent des protocoles propriétaires basés sur UDP, nous en parlerons donc ci-dessous.

Si vous décidez d'utiliser TCP, assurez-vous que l'algorithme Nagle est désactivé, car il met en mémoire tampon les paquets avant l'envoi, ce qui signifie qu'il augmente le délai.

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 propre


Vous souhaitez donc 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 pensées intelligentes.

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

Gardez à l'esprit que Glenn Fiedler est un grand partisan de l'utilisation de son propre protocole UDP. Et après avoir lu ses articles, vous allez sûrement vous remettre de son opinion que TCP a de sérieux inconvénients dans les jeux vidéo, et vous voulez mettre en œuvre votre propre protocole.

Mais si vous débutez en réseau, faites-vous plaisir et utilisez TCP ou une bibliothèque. Pour réussir à implémenter votre propre protocole de transport, vous devez d'abord en 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 soucier de mettre en œuvre votre propre protocole et entrer dans de nombreux détails, vous pouvez utiliser la bibliothèque réseau. Il y en a beaucoup:


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

Protocole de transport: conclusion


Pour résumer: il existe deux protocoles de transport principaux: TCP et UDP. TCP a de nombreuses fonctionnalités utiles: fiabilité, commande de paquets, détection d'erreurs. UDP n'a pas tout cela, mais TCP, par sa nature, a augmenté les retards qui sont inacceptables pour certains jeux. Autrement dit, pour garantir de faibles latences, vous pouvez créer votre propre protocole basé sur UDP ou utiliser une bibliothèque qui implémente le protocole de transport UDP et est adaptée aux jeux vidéo multi-joueurs.

Le choix entre TCP, UDP et la bibliothèque dépend de plusieurs facteurs. Tout d'abord, par rapport aux besoins du jeu: a-t-il besoin de faibles latences? Deuxièmement, à partir des exigences du protocole d'application: a-t-il besoin d'un protocole fiable? Comme nous le verrons dans la partie suivante, vous pouvez créer un protocole d'application pour lequel un protocole peu fiable est tout à fait approprié. Enfin, vous devez également prendre en compte l'expérience du développeur du moteur réseau.

J'ai deux conseils:

  • Maximisez le protocole de transport à partir du reste de l'application afin qu'il puisse être facilement remplacé sans réécrire tout le code.
  • Ne faites pas d'optimisation prématurée. Si vous n'êtes pas un spécialiste du réseau et ne savez pas si vous avez besoin de votre propre protocole de transport basé sur UDP, vous pouvez commencer avec TCP ou une bibliothèque qui fournit la fiabilité, puis tester et mesurer les performances. Si vous avez des problèmes et que vous êtes sûr que la raison réside dans le protocole de transport, le moment est peut-être venu de créer votre propre protocole de transport.

À la fin de cette partie, je vous recommande de lire l' introduction de Brian Hook à la programmation de jeux multijoueurs , qui couvre de nombreux sujets abordés ici.

Protocole d'application


Maintenant que nous pouvons échanger des données entre les clients et le serveur, nous devons décider quelles données transférer et dans quel format.

Le schéma classique est que les clients envoient des entrées ou des actions au serveur et que le serveur envoie l'état actuel du jeu aux clients.

Le serveur n'envoie pas un état complet mais filtré avec des entités situées à côté du lecteur. Il le fait pour trois raisons. Premièrement, l'état global peut être trop grand pour une transmission à haute fréquence. Deuxièmement, les clients sont principalement intéressés par les données visuelles et audio, car la majeure partie de la logique du jeu est simulée sur le serveur de jeu. Troisièmement, dans certains jeux, le joueur n'a pas besoin de connaître certaines données, par exemple la position de l'adversaire à l'autre bout de la carte, car sinon il peut renifler des paquets et savoir exactement où se déplacer pour le tuer.

Sérialisation


La première étape consiste à convertir les données que nous voulons envoyer (état d'entrée ou de jeu) dans un format adapté à la transmission. Ce processus est appelé sérialisation .

L'idée vient immédiatement à l'esprit d'utiliser un format lisible par l'homme, tel que JSON ou XML. Mais il sera totalement inefficace et occupera en vain l'essentiel du canal.

Au lieu de cela, il est recommandé d'utiliser un format binaire beaucoup plus compact. Autrement dit, les paquets ne contiendront que quelques octets. Ici, vous devez considérer le problème de l'ordre des octets , qui peut différer sur différents ordinateurs.

Vous pouvez utiliser une bibliothèque pour sérialiser des données, par exemple:


Assurez-vous simplement que la bibliothèque crée des archives portables et prend en charge l'ordre des octets.

Une solution indépendante peut être une implémentation indépendante, ce n'est pas particulièrement compliqué, surtout si vous utilisez une approche orientée données dans le code. De plus, il vous permettra d'effectuer des optimisations qui ne sont pas toujours possibles lors de l'utilisation de la bibliothèque.

Glenn Fiedler a écrit deux articles sur la sérialisation: lecture et écriture de paquets et stratégies de sérialisation .

La 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 vous permet de transférer plus de données dans chaque instantané, d'augmenter le taux de rafraîchissement ou simplement de réduire les exigences de canal.

Emballage de bits


La première technique est le compactage des bits. Elle consiste à utiliser exactement le nombre de bits nécessaire pour décrire la valeur souhaitée. Par exemple, si vous avez une énumération qui peut avoir 16 valeurs différentes, alors au lieu d'un octet entier (8 bits), vous ne pouvez utiliser que 4 bits.

Glenn Fiedler explique comment implémenter cela dans la deuxième partie de l'article sur les paquets de lecture et d'écriture .

L'emballage de bits fonctionne particulièrement bien avec l'échantillonnage, qui sera le sujet de la section suivante.

Discrétisation


La discrétisation est une technique de compression avec perte qui n'utilise qu'un sous-ensemble des valeurs possibles pour coder une valeur. La façon la plus simple de mettre en œuvre la discrétisation consiste à arrondir les nombres à virgule flottante.

Glenn Fiedler (encore!) Montre comment appliquer l'échantillonnage dans la pratique dans son article sur la compression d'instantané .

Algorithmes de compression


La prochaine technique sera les algorithmes de compression sans perte.

Voici, à mon avis, les trois algorithmes les plus intéressants que vous devez connaître:

  • Codage Huffman avec du code pré-calculé extrêmement rapide et pouvant donner de bons résultats. Il a été utilisé pour compresser les paquets dans le moteur réseau Quake3.
  • zlib est un algorithme de compression à usage général qui n'augmente jamais la quantité de données. Comme on peut le voir ici , il a été utilisé dans de nombreuses applications. Il peut être redondant de mettre à jour les états. Mais cela peut être utile si vous devez envoyer des actifs, des textes longs ou des secours aux clients à partir du serveur.
  • La copie de longueurs de 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é à la compression de terrains constitués de tuiles ou de voxels, dans lesquels de nombreux éléments voisins se répètent.

Compression delta


La dernière technique de compression est la compression delta. Elle réside dans le fait que seules les différences entre l'état actuel du jeu et le dernier état reçu par le client sont transmises.

Il a d'abord été utilisé dans le moteur de réseau Quake3. Voici deux articles expliquant comment l'utiliser:


Glenn Fiedler l'a également utilisé dans la deuxième partie de son article sur la compression d'images .

Cryptage


En outre, vous devrez peut-être crypter le transfert d'informations entre les clients et le serveur. Il y a plusieurs raisons à cela:

  • confidentialité / confidentialité: les messages ne peuvent être lus que par le destinataire, et aucune autre personne reniflant le réseau ne peut les lire.
  • authentification: une personne qui veut jouer le rôle d'un joueur doit connaître sa clé.
  • prévention de la triche: il sera beaucoup plus difficile pour les joueurs malveillants de créer leurs propres paquets de triche, ils devront reproduire le schéma de cryptage et trouver la clé (qui change à chaque connexion).

Je recommande fortement d'utiliser la bibliothèque pour cela. Je suggère d'utiliser libsodium car il est particulièrement simple et propose d'excellents tutoriels. Le didacticiel d' échange de clés est particulièrement intéressant. Il vous permet de générer de nouvelles clés à chaque nouvelle connexion.

Protocole d'application: conclusion


Nous terminerons avec le protocole d'application. Je pense que la compression est complètement facultative et la décision de l'utiliser ne dépend que du jeu et de la bande passante requise. Le cryptage, à mon avis, est obligatoire, mais dans le premier prototype, vous pouvez vous en passer.

Logique d'application


Nous pouvons maintenant mettre à jour l'état du client, mais nous pouvons rencontrer des problèmes de retard. Une fois entré, le joueur doit attendre la mise à jour de l'état du jeu depuis le serveur pour voir quel impact il a eu sur le monde.

De plus, entre deux mises à jour d'état, le monde est complètement statique. Si le taux de rafraîchissement des états est faible, les mouvements seront très saccadés.

Il existe plusieurs techniques pour réduire l'impact de ce problème, et dans la section suivante, j'en parlerai.

Techniques de lissage différé


Toutes les techniques décrites dans cette section sont discutées en détail dans la série multijoueur à rythme rapide de Gabriel Gambetta. Je recommande fortement de lire cette grande série d'articles. Il dispose également d'une démonstration interactive qui vous permet de voir comment ces techniques fonctionnent dans la pratique.

La première technique consiste à appliquer directement l'entrée, sans attendre de réponse du serveur. C'est ce qu'on appelle la prédiction côté client . Cependant, lorsque le client reçoit la mise à jour du serveur, il doit s'assurer que sa prévision était correcte. Si ce n'est pas le cas, il lui suffit de changer son état en fonction de celui reçu du serveur, car le serveur est autoritaire. Cette technique a été utilisée pour la première fois à Quake. Vous pouvez en savoir plus à ce sujet dans l'article Revue du code Quake Engine par Fabien Sanglar.

Le deuxième ensemble de techniques est utilisé pour faciliter le mouvement d'autres entités entre deux mises à jour d'état. Il existe deux façons de résoudre ce problème: l'interpolation et l'extrapolation. En cas d'interpolation, les deux derniers états sont pris et la transition de l'un à l'autre est affichée. Son inconvénient est qu'il cause une petite fraction du retard, car le client voit toujours ce qui s'est passé dans le passé. L'extrapolation prédit où les entités devraient désormais être basées sur le dernier état reçu par le client. Son inconvénient est que si l'entité change complètement la direction du mouvement, alors 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 en FPS est la compensation de décalage . Lors de l'utilisation de la compensation de décalage, le serveur prend en compte les retards du client lorsqu'il tire sur une cible. Par exemple, si un joueur a effectué un tir à la tête sur son écran, mais en réalité son objectif était situé ailleurs en raison du retard, il serait alors malhonnête de refuser au joueur le droit de tuer en raison du retard. Par conséquent, le serveur rembobine le temps jusqu'au moment où le joueur a tiré pour simuler ce que le joueur a vu sur son écran et vérifier la collision entre son tir et la cible.

Glenn Fiedler (comme toujours!) A écrit un article de 2004 dans Network Physics (2004) , qui a jeté les bases de la synchronisation des simulations physiques entre un serveur et un client. En 2014, il a écrit une nouvelle série d'articles sur la physique des réseaux qui décrivaient d'autres techniques de synchronisation des simulations physiques.

Il y a également deux articles sur le wiki Valve, Source Multiplayer Networking et Latency Compensating Methods in Client / Server In-game Protocol Design and Optimization , qui traitent de la compensation des retards.

Prévention de la triche


Il existe deux techniques principales pour éviter la tricherie.

Premièrement: compliquer l'envoi de colis malveillants par des tricheurs. Comme indiqué ci-dessus, le cryptage est un bon moyen de le mettre en œuvre.

Deuxièmement: un serveur autoritaire ne devrait recevoir que des commandes / entrées / actions. Le client ne doit pas pouvoir modifier l'état sur le serveur, sauf en envoyant des entrées. Ensuite, chaque fois que l'entrée est reçue, le serveur doit en vérifier la validité avant de l'appliquer.

Logique d'application: conclusion


Je vous recommande d'implémenter une méthode de simulation de retards importants et de faibles taux de rafraîchissement afin de pouvoir tester le comportement de votre jeu dans de mauvaises conditions, même lorsque le client et le serveur fonctionnent sur le même ordinateur. Cela simplifiera considérablement la mise en œuvre des techniques de lissage des retards.

Autres ressources utiles


Si vous souhaitez explorer d'autres ressources sur les modèles de réseau, vous pouvez les trouver ici:

Source: https://habr.com/ru/post/fr467025/


All Articles