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

À propos du modèle de réseau dans les jeux pour débutants
Au cours des deux dernières semaines, j'ai travaillé sur le moteur de mise en réseau de mon jeu. Avant cela, je ne connaissais rien du tout à la mise en réseau dans les jeux, j'ai donc lu beaucoup d'articles et fait beaucoup d'expériences pour comprendre tous les concepts et pouvoir écrire mon propre moteur de mise en réseau.

Dans ce guide, j'aimerais partager avec vous les différents 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 réseau : peer-to-peer et client-serveur. Dans une architecture peer-to-peer (p2p), les données sont transférées entre n'importe quelle paire de joueurs connectés, tandis que dans une architecture client-serveur, les données sont transférées uniquement entre les joueurs et le serveur.

Bien que l'architecture peer-to-peer soit encore utilisée dans certains jeux, l'architecture client-serveur est la norme : elle est plus facile à mettre en œuvre, 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 le joueur pense qu'il est en (10, 5) et que le serveur lui dit qu'il est en (5, 3), alors le client doit remplacer sa position par celle que le serveur rapporte, et non l'inverse. L'utilisation de serveurs faisant autorité facilite la reconnaissance des tricheurs.

Il existe trois composants principaux dans les systèmes de réseau de jeu :

  • 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 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 partie et les difficultés qui y sont associées.

Protocole de transport

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

Comparaison de TCP et UDP

TCP et UDP sont basés sur IP. IP permet à un paquet d'être transmis d'une source à un récepteur, mais il ne garantit pas que le paquet envoyé parviendra tôt ou tard au récepteur, qu'il y parviendra au moins une fois et que la séquence de paquets arrivera dans la bonne commande. De plus, un paquet ne peut contenir qu'une taille de données limitée, donnée par la valeur MTU.

UDP n'est qu'une fine couche au-dessus d'IP. Il a donc les mêmes limites. 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, dans HTTP, Ftp и SMTP. Mais toutes ces fonctionnalités ont un prix : retard.

Pour comprendre pourquoi ces fonctions peuvent provoquer une latence, nous devons comprendre le fonctionnement de TCP. Lorsque l'hôte émetteur transmet un paquet à l'hôte 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 une autre raison), alors il renvoie le paquet. De plus, TCP garantit que les paquets sont reçus dans le bon ordre, donc 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 l'avez probablement compris, la latence dans les jeux multijoueurs est très importante, en particulier dans des genres aussi actifs que les FPS. C'est pourquoi de nombreux jeux utilisent UDP avec son 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 fiables et d'autres comme non fiables. Par conséquent, il ne se soucie pas de savoir si le paquet non fiable a atteint le destinataire. Ou il peut traiter plusieurs flux de données afin qu'un paquet perdu dans un flux ne ralentisse pas les autres flux. Par exemple, il peut y avoir un fil pour les entrées des joueurs et un autre fil pour les messages de chat. Si un message de chat qui n'est pas une donnée urgente est perdu, il ne ralentira pas la saisie urgente. Ou un protocole propriétaire peut implémenter la fiabilité différemment de TCP pour être plus efficace dans un environnement de jeu vidéo.

Donc, si TCP craint, alors nous allons construire 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 réseau de jeu, il peut très bien fonctionner pour votre jeu spécifique et vous faire gagner 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 ne peut être joué que sur des réseaux LAN, où la latence et la perte de paquets sont bien moindres que sur Internet.

De nombreux jeux à succès, dont World of Warcraft, Minecraft et Terraria, utilisent TCP. Cependant, la plupart des FPS utilisent leurs propres protocoles basés sur UDP, nous en parlerons donc plus en détail ci-dessous.

Si vous choisissez d'utiliser TCP, assurez-vous qu'il est désactivé Algorithme de Nagle, car il met les paquets en mémoire tampon avant de les envoyer, 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, consultez l'article de Glenn Fiedler UDP contre TCP.

Protocole propriétaire

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

Premier article Réseautage pour les programmeurs de jeux 2008, plus facile que le second Construire un protocole de réseau de jeu 2016. Je vous conseille de commencer par l'ancien.

Sachez que Glenn Fiedler est un grand partisan de l'utilisation de votre propre protocole basé sur UDP. Et après avoir lu ses articles, vous adopterez probablement son opinion selon laquelle TCP présente de sérieux inconvénients dans les jeux vidéo, et vous souhaiterez implémenter votre propre protocole.

Mais si vous débutez dans le domaine des réseaux, rendez-vous service et utilisez TCP ou une bibliothèque. Pour implémenter avec succès votre propre protocole de transport, vous devez en apprendre beaucoup au préalable.

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 entrer dans beaucoup de détails, vous pouvez utiliser la bibliothèque net. 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, il dispose d'une documentation claire et d'un tutoriel pour les débutants.

Conclusion du protocole de transport

Pour résumer, il existe deux principaux protocoles de transport : TCP et UDP. TCP a de nombreuses fonctionnalités utiles : fiabilité, préservation de l'ordre des paquets, détection d'erreurs. UDP n'a pas tout cela, mais TCP, de par sa nature même, a une latence élevée qui est inacceptable pour certains jeux. Autrement dit, pour garantir une faible latence, vous pouvez créer votre propre protocole basé sur UDP ou utiliser une bibliothèque qui implémente le protocole de transport sur UDP et est adaptée aux jeux vidéo multijoueurs.

Le choix entre TCP, UDP et la bibliothèque dépend de plusieurs facteurs. Premièrement, à partir des besoins du jeu : a-t-il besoin d'une faible latence ? 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, il est possible de créer un protocole d'application pour lequel un protocole non fiable convient tout à fait. Enfin, vous devez également tenir compte de l'expérience du développeur du moteur de réseau.

J'ai deux astuces :

  • Extrayez autant que possible le protocole de transport du reste de l'application afin qu'il puisse être facilement remplacé sans réécrire tout le code.
  • Ne pas trop optimiser. Si vous n'êtes pas un expert en réseau et que vous n'êtes pas sûr d'avoir besoin de votre propre protocole de transport basé sur UDP, vous pouvez commencer par TCP ou une bibliothèque qui assure la fiabilité, puis tester et mesurer les performances. Si vous rencontrez des problèmes et que vous êtes certain qu'il s'agit d'un protocole de transport, il est peut-être temps de créer votre propre protocole de transport.

À la fin de cette partie, je vous recommande de lire Introduction à la programmation de jeux multijoueurs Brian Hook, 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 le serveur envoie l'état actuel du jeu aux clients.

Le serveur n'envoie pas l'état complet, mais l'état filtré avec les entités proches du joueur. Il le fait pour trois raisons. Premièrement, l'état total peut être trop grand pour être transmis à 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, comme la position de l'ennemi de l'autre côté de la carte, car sinon il peut renifler les 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 (entrée ou état du 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 cela sera complètement inefficace et occupera la majeure partie du canal pour rien.

Au lieu de cela, il est recommandé d'utiliser le format binaire, qui est beaucoup plus compact. Autrement dit, les paquets ne contiendront que quelques octets. Ici, il faut tenir compte du problème ordre des octets, qui peuvent différer d'un ordinateur à l'autre.

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

Assurez-vous simplement que la bibliothèque crée des archives portables et s'occupe de l'endianité.

Une solution alternative serait de l'implémenter vous-même, ce n'est pas si difficile, surtout si vous utilisez une approche centrée sur les données dans votre code. De plus, cela 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 : Paquets de lecture et d'écriture и 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 vous permettra de transférer plus de données dans chaque instantané, d'augmenter le taux de rafraîchissement ou simplement de réduire les besoins en bande passante.

Peu d'emballage

La première technique est le bit packing. 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 pouvez utiliser seulement 4 bits.

Glenn Fiedler explique comment mettre cela en œuvre dans la deuxième partie de l'article. Paquets de lecture et d'écriture.

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

Échantillonnage

Échantillonnage est une technique de compression avec perte qui utilise uniquement un sous-ensemble de valeurs possibles pour coder une valeur. Le moyen le plus simple d'implémenter la discrétisation consiste à arrondir les nombres à virgule flottante.

Glenn Fiedler (encore !) montre comment appliquer la discrétisation en pratique dans son article Compression d'instantanés.

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 de Huffman avec du code précalculé, qui est extrêmement rapide et peut produire de bons résultats. Il était utilisé pour compresser les paquets dans le moteur de réseau Quake3.
  • zlib est un algorithme de compression à usage général qui n'augmente jamais la quantité de données. Comment peux-tu voir ici, il a été utilisé dans une variété d'applications. Pour la mise à jour des états, il peut être redondant. Mais cela peut s'avérer utile si vous devez envoyer des ressources, des textes longs ou du terrain à des clients depuis le serveur.
  • Copier des longueurs de tirage 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 été utilisé pour la première fois 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. Compression d'instantanés.

Шифрование

De plus, vous devrez peut-être crypter la transmission d'informations entre les clients et le serveur. Il y a plusieurs raisons à cela:

  • Intimité/confidentialité : les messages ne peuvent être lus que par le destinataire et aucun autre renifleur de réseau ne pourra 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 packages de triche, ils devront répliquer le schéma de chiffrement et trouver la clé (qui change à chaque connexion).

Je recommande fortement d'utiliser une bibliothèque pour cela. Je suggère d'utiliser libsodium, car il est particulièrement simple et contient d'excellents didacticiels. Particulièrement intéressant est le tutoriel sur échange de clés, qui permet de générer de nouvelles clés à chaque nouvelle connexion.

Protocole de candidature : conclusion

Ceci conclut le protocole d'application. Je pense que la compression est totalement facultative et que 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 sommes maintenant en mesure de mettre à jour l'état dans le client, mais nous pouvons rencontrer des problèmes de latence. Le joueur, après avoir fait une entrée, doit attendre une mise à jour de l'état du jeu du serveur pour voir quel effet cela a eu sur le monde.

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

Il existe plusieurs techniques pour atténuer l'impact de ce problème, et je les aborderai dans la section suivante.

Techniques de lissage différé

Toutes les techniques décrites dans cette section sont discutées en détail dans la série. Multijoueur rapide Gabriel Gambette. Je recommande vivement la lecture de cette excellente série d'articles. Il comprend également une démonstration interactive pour voir comment ces techniques fonctionnent dans la pratique.

La première technique consiste à appliquer directement le résultat d'entrée sans attendre une réponse du serveur. On l'appelle prédiction côté client. Cependant, lorsque le client reçoit une mise à jour du serveur, il doit vérifier que sa prédiction était correcte. Si ce n'est pas le cas, alors il lui suffit de changer son état en fonction de ce qu'il a reçu du serveur, car le serveur est autoritaire. Cette technique a été utilisée pour la première fois dans Quake. Vous pouvez en savoir plus à ce sujet dans l'article. Révision du code de Quake Engine Fabien Sanglars [traduction sur Habré].

Le deuxième ensemble de techniques est utilisé pour lisser 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. Dans le cas de l'interpolation, les deux derniers états sont pris et la transition de l'un à l'autre est représentée. Son inconvénient est qu'il n'entraîne qu'une petite fraction du retard, car le client voit toujours ce qui s'est passé dans le passé. L'extrapolation consiste à prédire où les entités devraient être maintenant en fonction du dernier état reçu par le client. Son inconvénient est que si l'entité change complètement la direction du mouvement, 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 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 se déclenche sur la cible. Par exemple, si un joueur a effectué un tir à la tête sur son écran, mais qu'en réalité sa cible se trouvait à un endroit différent en raison du retard, il serait injuste de refuser au joueur le droit de tuer en raison du retard. Ainsi, 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 s'il y a une collision entre son tir et la cible.

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

Il y a aussi deux articles sur le wiki de Valve, Réseau multijoueur source и Méthodes de compensation de latence dans la conception et l'optimisation de protocoles client/serveur dans le jeu traitant de l'indemnisation des retards.

Prévention de la triche

Il existe deux principales techniques de prévention de la triche.

Premièrement, il est plus difficile pour les tricheurs d'envoyer des paquets malveillants. Comme mentionné ci-dessus, un bon moyen de l'implémenter est le chiffrement.

Deuxièmement, le serveur faisant autorité ne doit recevoir que des commandes/entrées/actions. Le client ne doit pas pouvoir modifier l'état du serveur autrement qu'en envoyant une entrée. Ensuite, le serveur, chaque fois qu'il reçoit une entrée, doit en vérifier la validité avant de l'appliquer.

Logique d'application : conclusion

Je vous recommande de mettre en place un moyen de simuler une latence élevée et des taux de rafraîchissement faibles afin de pouvoir tester le comportement de votre jeu dans de mauvaises conditions, même lorsque le client et le serveur s'exécutent sur la même machine. Cela simplifie grandement la mise en œuvre des techniques de lissage de retard.

Autres ressources utiles

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

Source: habr.com

Ajouter un commentaire